openapi: 3.1.0
info:
  title: whatsmeow-api
  version: 0.1.0
  description: |
    Multi-tenant WhatsApp platform. Path-based tenancy: all authenticated endpoints
    live under `/{tenant_slug}/api/v1/*`. Authentication is either JWT (for
    attendants) or API key (for integrations), both via `Authorization: Bearer`.

    API keys have prefix `wsk_` which distinguishes them from JWTs at the gateway.
  contact:
    name: Alessandro Mata
    email: alessandro.mata@gmail.com

servers:
  - url: https://whatsmeow.vitrinefashion.app
    description: Production
  - url: http://localhost:8080
    description: Local dev

security:
  - BearerAuth: []
  - ApiKeyAuth: []

tags:
  - name: Auth
    description: Login, token refresh, logout, invite acceptance
  - name: Users
    description: Tenant user management (admin/gerente)
  - name: API Keys
    description: Integration API key lifecycle (admin)
  - name: Sessions
    description: WhatsApp session (device) management
  - name: Contacts
    description: WhatsApp contact directory
  - name: Conversations
    description: Conversation inbox and assignment
  - name: Messages
    description: Inbound/outbound message history and sending
  - name: AI Agents
    description: Per-tenant LLM agent configuration (provider, prompt, golden rules)
  - name: Risk Words
    description: Compliance word filter with handoff/alert/block actions
  - name: Bot
    description: Bot session and conversation state toggles (auto-reply, bot-mode)
  - name: Webhooks
    description: Outgoing webhook subscriptions
  - name: Media
    description: Stored media blob download
  - name: System
    description: Health checks and WebSocket hub
  - name: Superadmin
    description: Platform-level admin panel — not tenant-scoped
  - name: Quick Replies
    description: Reusable message templates with optional shortcuts (e.g. /bomdia)
  - name: Scheduled Messages
    description: One-time scheduled 1-to-1 messages
  - name: Campaigns
    description: Bulk message dispatching to lists of WhatsApp JIDs (admin-only)

paths:

  # ─── System ────────────────────────────────────────────────────────────────

  /healthz:
    get:
      summary: Liveness probe
      description: Always returns 200. No dependency checks.
      tags: [System]
      security: []
      responses:
        '200':
          description: Service is alive
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, example: ok }

  /ws:
    get:
      summary: WebSocket hub
      description: |
        Upgrade to WebSocket. JWT must be passed as `?access_token=<token>` because
        browsers cannot set Authorization headers on WS connections.
        Emits real-time events: `qr_code`, `session_status`, `message`, `conversation_update`.
      tags: [System]
      security: []
      parameters:
        - name: access_token
          in: query
          required: true
          schema: { type: string }
          description: JWT access token
      responses:
        '101':
          description: Switching protocols — WebSocket upgrade accepted
        '401':
          $ref: '#/components/responses/Unauthorized'

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

  /{slug}/api/v1/auth/login:
    post:
      summary: Login
      description: |
        Authenticates a user and returns a short-lived JWT access token plus a
        `refresh_token` HttpOnly cookie (14-day TTL). Rate-limited to 5 req/min
        per IP+email.
      tags: [Auth]
      security: []
      parameters:
        - $ref: '#/components/parameters/TenantSlug'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/LoginRequest'
            example:
              email: admin@example.com
              password: "s3cr3t!"
      responses:
        '200':
          description: Authenticated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AuthResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'

  /{slug}/api/v1/auth/refresh:
    post:
      summary: Refresh access token
      description: |
        Rotates the refresh token (cookie) and returns a new access JWT.
        Requires a valid `refresh_token` HttpOnly cookie.
      tags: [Auth]
      security: []
      parameters:
        - $ref: '#/components/parameters/TenantSlug'
      responses:
        '200':
          description: New access token issued
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AuthResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'

  /{slug}/api/v1/auth/logout:
    post:
      summary: Logout
      description: Revokes the refresh token and clears the cookie. Idempotent.
      tags: [Auth]
      parameters:
        - $ref: '#/components/parameters/TenantSlug'
      responses:
        '204':
          description: Logged out

  /{slug}/api/v1/auth/accept-invite:
    post:
      summary: Accept invite and set password
      description: |
        Validates the invite token, sets the user's password, activates the account,
        and returns an access token. Token is single-use and expires in 7 days.
      tags: [Auth]
      security: []
      parameters:
        - $ref: '#/components/parameters/TenantSlug'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AcceptInviteRequest'
      responses:
        '200':
          description: Account activated and logged in
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AuthResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'

  # ─── Users ─────────────────────────────────────────────────────────────────

  /{slug}/api/v1/users:
    get:
      summary: List users
      description: Returns all users for the tenant. Roles admin and gerente only.
      tags: [Users]
      parameters:
        - $ref: '#/components/parameters/TenantSlug'
      responses:
        '200':
          description: User list
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/User'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'

  /{slug}/api/v1/users/invite:
    post:
      summary: Invite a user
      description: |
        Creates an invited user and sends an onboarding email (if Resend is configured).
        Returns the invite token once — store it or share the link. Admin only.
      tags: [Users]
      parameters:
        - $ref: '#/components/parameters/TenantSlug'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/InviteUserRequest'
            example:
              email: nova@loja.com.br
              display_name: Maria Silva
              role: atendente
      responses:
        '201':
          description: Invitation created
          content:
            application/json:
              schema:
                type: object
                properties:
                  id: { type: string, format: uuid }
                  invite_token: { type: string }
                  expires_at: { type: string, format: date-time }
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'

  /{slug}/api/v1/users/{id}:
    patch:
      summary: Update user role or status
      description: Change a user's role or disable them. Admin only.
      tags: [Users]
      parameters:
        - $ref: '#/components/parameters/TenantSlug'
        - $ref: '#/components/parameters/ResourceID'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                role:
                  type: string
                  enum: [admin, gerente, atendente]
                status:
                  type: string
                  enum: [disabled]
      responses:
        '204':
          description: Updated
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
    delete:
      summary: Disable (soft-delete) a user
      description: Sets status to disabled. Admin only.
      tags: [Users]
      parameters:
        - $ref: '#/components/parameters/TenantSlug'
        - $ref: '#/components/parameters/ResourceID'
      responses:
        '204':
          description: Disabled
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'

  # ─── API Keys ──────────────────────────────────────────────────────────────

  /{slug}/api/v1/apikeys:
    get:
      summary: List API keys
      description: Returns all non-revoked API keys (secret hash masked). Admin only.
      tags: [API Keys]
      parameters:
        - $ref: '#/components/parameters/TenantSlug'
      responses:
        '200':
          description: Key list
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/APIKey'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
    post:
      summary: Create API key
      description: |
        Generates a new API key. The plaintext `key_plaintext` field (format `wsk_<hex>`)
        is returned **once** — store it immediately. Admin only.
      tags: [API Keys]
      parameters:
        - $ref: '#/components/parameters/TenantSlug'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateAPIKeyRequest'
            example:
              name: n8n integration
              scopes: ["messages:send", "conversations:read"]
      responses:
        '201':
          description: Key created — save key_plaintext now, it will not be shown again
          content:
            application/json:
              schema:
                type: object
                properties:
                  id: { type: string, format: uuid }
                  key_plaintext: { type: string, example: "wsk_a1b2c3d4..." }
                  prefix: { type: string, example: "wsk_a1b2" }
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'

  /{slug}/api/v1/apikeys/{id}:
    delete:
      summary: Revoke API key
      description: Sets revoked_at=now(). Subsequent requests using this key return 401. Admin only.
      tags: [API Keys]
      parameters:
        - $ref: '#/components/parameters/TenantSlug'
        - $ref: '#/components/parameters/ResourceID'
      responses:
        '204':
          description: Revoked
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'

  # ─── Sessions ──────────────────────────────────────────────────────────────

  /{slug}/api/v1/sessions:
    get:
      summary: List WhatsApp sessions
      description: Returns all sessions for the tenant.
      tags: [Sessions]
      parameters:
        - $ref: '#/components/parameters/TenantSlug'
      responses:
        '200':
          description: Session list
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/Session'
        '401':
          $ref: '#/components/responses/Unauthorized'
    post:
      summary: Create session
      description: |
        Creates a session DB record and whatsmeow device but does **not** connect.
        Call `POST /{slug}/api/v1/sessions/{id}/connect` to initiate the QR flow.
        Roles: admin, gerente.
      tags: [Sessions]
      parameters:
        - $ref: '#/components/parameters/TenantSlug'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateSessionRequest'
      responses:
        '201':
          description: Session created
          content:
            application/json:
              schema:
                type: object
                properties:
                  id: { type: string, format: uuid }
                  status: { type: string, example: pending_qr }
        '400':
          $ref: '#/components/responses/BadRequest'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'

  /{slug}/api/v1/sessions/{id}:
    get:
      summary: Get session
      description: Returns a single session scoped to the current tenant.
      tags: [Sessions]
      parameters:
        - $ref: '#/components/parameters/TenantSlug'
        - $ref: '#/components/parameters/ResourceID'
      responses:
        '200':
          description: Session
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Session'
        '404':
          $ref: '#/components/responses/NotFound'
    delete:
      summary: Delete session
      description: Removes session from memory and database. Admin only.
      tags: [Sessions]
      parameters:
        - $ref: '#/components/parameters/TenantSlug'
        - $ref: '#/components/parameters/ResourceID'
      responses:
        '204':
          description: Deleted
        '404':
          $ref: '#/components/responses/NotFound'

  /{slug}/api/v1/sessions/{id}/connect:
    post:
      summary: Connect session
      description: |
        Initiates a background connection attempt. QR codes are pushed via WebSocket
        (`/ws`) as `qr_code` events. Returns immediately with `{"status":"connecting"}`.
        Roles: admin, gerente.
      tags: [Sessions]
      parameters:
        - $ref: '#/components/parameters/TenantSlug'
        - $ref: '#/components/parameters/ResourceID'
      responses:
        '202':
          description: Connection initiated
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, example: connecting }
        '404':
          $ref: '#/components/responses/NotFound'

  /{slug}/api/v1/sessions/{id}/qr-sync:
    post:
      summary: Synchronous QR code fetch
      description: |
        Blocks up to 30s waiting for the first QR code. Useful for CLI pairing
        without a WebSocket client. Production flows should use `/ws` instead.
      tags: [Sessions]
      parameters:
        - $ref: '#/components/parameters/TenantSlug'
        - $ref: '#/components/parameters/ResourceID'
      responses:
        '200':
          description: QR code string (base64 PNG or raw code)
          content:
            application/json:
              schema:
                type: object
                properties:
                  qr_code: { type: string }
                  status: { type: string, enum: [already_connected, qr_expired] }
        '404':
          $ref: '#/components/responses/NotFound'
        '504':
          description: QR timeout (>30s)

  /{slug}/api/v1/sessions/{id}/disconnect:
    post:
      summary: Disconnect session
      description: Disconnects TCP without logging out. The device stays paired; reconnect resumes.
      tags: [Sessions]
      parameters:
        - $ref: '#/components/parameters/TenantSlug'
        - $ref: '#/components/parameters/ResourceID'
      responses:
        '200':
          description: Disconnected
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, example: disconnected }

  /{slug}/api/v1/sessions/{id}/logout:
    post:
      summary: Logout session from WhatsApp
      description: Logs the device out of WhatsApp (equivalent to "Log out" in WA). Device is unpaired.
      tags: [Sessions]
      parameters:
        - $ref: '#/components/parameters/TenantSlug'
        - $ref: '#/components/parameters/ResourceID'
      responses:
        '200':
          description: Logged out
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, example: logged_out }

  /{slug}/api/v1/sessions/{id}/pair-phone:
    post:
      summary: Pair via phone number
      description: Generates a 8-digit pairing code for phone-based linking (no QR scan needed).
      tags: [Sessions]
      parameters:
        - $ref: '#/components/parameters/TenantSlug'
        - $ref: '#/components/parameters/ResourceID'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [phone]
              properties:
                phone:
                  type: string
                  description: "International format, e.g. 5511999990000"
                  example: "5511999990000"
      responses:
        '200':
          description: Pairing code
          content:
            application/json:
              schema:
                type: object
                properties:
                  code: { type: string, example: "1234-5678" }
        '400':
          $ref: '#/components/responses/BadRequest'
        '404':
          $ref: '#/components/responses/NotFound'

  /{slug}/api/v1/sessions/{id}/auto-reply:
    put:
      summary: Toggle session auto-reply
      description: Enables or disables the bot's auto-reply on a session. Roles: admin, gerente.
      tags: [Bot]
      parameters:
        - $ref: '#/components/parameters/TenantSlug'
        - $ref: '#/components/parameters/ResourceID'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [enabled]
              properties:
                enabled: { type: boolean }
      responses:
        '204':
          description: Updated

  # ─── Contacts ──────────────────────────────────────────────────────────────

  /{slug}/api/v1/contacts:
    get:
      summary: List contacts
      description: Returns contacts for the tenant. Optionally filtered by session_id.
      tags: [Contacts]
      parameters:
        - $ref: '#/components/parameters/TenantSlug'
        - name: session_id
          in: query
          schema: { type: string, format: uuid }
          description: Filter by session
      responses:
        '200':
          description: Contact list
          content:
            application/json:
              schema:
                type: object
                properties:
                  contacts:
                    type: array
                    items:
                      $ref: '#/components/schemas/Contact'

  /{slug}/api/v1/contacts/{id}:
    get:
      summary: Get contact
      tags: [Contacts]
      parameters:
        - $ref: '#/components/parameters/TenantSlug'
        - $ref: '#/components/parameters/ResourceID'
      responses:
        '200':
          description: Contact
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Contact'
        '404':
          $ref: '#/components/responses/NotFound'
    patch:
      summary: Update contact display name
      description: Overrides the display name shown in the UI. Roles: admin, gerente.
      tags: [Contacts]
      parameters:
        - $ref: '#/components/parameters/TenantSlug'
        - $ref: '#/components/parameters/ResourceID'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                display_name: { type: string }
      responses:
        '200':
          description: Updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, example: ok }

  /{slug}/api/v1/contacts/{id}/picture:
    get:
      summary: Download contact profile picture
      description: Returns the raw image bytes with the correct Content-Type. 404 if not yet fetched.
      tags: [Contacts]
      parameters:
        - $ref: '#/components/parameters/TenantSlug'
        - $ref: '#/components/parameters/ResourceID'
      responses:
        '200':
          description: Image bytes
          content:
            image/jpeg:
              schema: { type: string, format: binary }
            image/png:
              schema: { type: string, format: binary }
        '404':
          $ref: '#/components/responses/NotFound'

  /{slug}/api/v1/contacts/{id}/refresh-picture:
    post:
      summary: Refresh contact profile picture
      description: |
        Synchronously fetches the latest profile picture from WhatsApp (30s timeout).
        Roles: admin, gerente.
      tags: [Contacts]
      parameters:
        - $ref: '#/components/parameters/TenantSlug'
        - $ref: '#/components/parameters/ResourceID'
      responses:
        '200':
          description: Refreshed
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, example: ok }
        '404':
          $ref: '#/components/responses/NotFound'
        '502':
          description: WhatsApp network error or timeout

  # ─── Conversations ─────────────────────────────────────────────────────────

  /{slug}/api/v1/conversations:
    get:
      summary: List conversations
      description: |
        Returns conversations for the tenant. Atendentes see only their assigned
        conversations; admin/gerente see all (optionally filtered).
      tags: [Conversations]
      parameters:
        - $ref: '#/components/parameters/TenantSlug'
        - name: status
          in: query
          schema:
            type: array
            items:
              type: string
              enum: [queued, open, closed, bot]
          style: form
          explode: true
          description: Filter by status (repeatable)
        - name: session_id
          in: query
          schema: { type: string, format: uuid }
        - name: assigned_to
          in: query
          schema: { type: string }
          description: UUID of user, or literal "me"
        - name: limit
          in: query
          schema: { type: integer, default: 50, maximum: 200 }
        - name: before
          in: query
          schema: { type: string, format: date-time }
          description: Cursor — return conversations older than this timestamp
      responses:
        '200':
          description: Conversation list
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/ConversationPreview'

  /{slug}/api/v1/conversations/{id}:
    get:
      summary: Get conversation
      description: |
        Returns a single conversation. Atendentes are forbidden from seeing
        conversations not assigned to them.
      tags: [Conversations]
      parameters:
        - $ref: '#/components/parameters/TenantSlug'
        - $ref: '#/components/parameters/ResourceID'
      responses:
        '200':
          description: Conversation
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Conversation'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'

  /{slug}/api/v1/conversations/{id}/claim:
    post:
      summary: Claim conversation
      description: |
        Atomically claims a queued conversation for the authenticated attendant.
        Returns 409 if already claimed.
      tags: [Conversations]
      parameters:
        - $ref: '#/components/parameters/TenantSlug'
        - $ref: '#/components/parameters/ResourceID'
      responses:
        '204':
          description: Claimed
        '401':
          $ref: '#/components/responses/Unauthorized'
        '409':
          description: Already claimed by another user
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  /{slug}/api/v1/conversations/{id}/assign:
    post:
      summary: Assign conversation
      description: Force-assigns a conversation to a specific user. Roles: admin, gerente.
      tags: [Conversations]
      parameters:
        - $ref: '#/components/parameters/TenantSlug'
        - $ref: '#/components/parameters/ResourceID'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [user_id]
              properties:
                user_id: { type: string, format: uuid }
      responses:
        '204':
          description: Assigned
        '400':
          $ref: '#/components/responses/BadRequest'

  /{slug}/api/v1/conversations/{id}/transfer:
    post:
      summary: Transfer conversation
      description: Reassigns and notifies the previous assignee. Roles: admin, gerente.
      tags: [Conversations]
      parameters:
        - $ref: '#/components/parameters/TenantSlug'
        - $ref: '#/components/parameters/ResourceID'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [user_id]
              properties:
                user_id: { type: string, format: uuid }
      responses:
        '204':
          description: Transferred
        '400':
          $ref: '#/components/responses/BadRequest'

  /{slug}/api/v1/conversations/{id}/close:
    post:
      summary: Close conversation
      description: |
        Sets status to closed. Atendentes may only close their own conversations;
        admin/gerente may close any.
      tags: [Conversations]
      parameters:
        - $ref: '#/components/parameters/TenantSlug'
        - $ref: '#/components/parameters/ResourceID'
      responses:
        '204':
          description: Closed
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'

  /{slug}/api/v1/conversations/{id}/bot-mode:
    put:
      summary: Set conversation bot mode
      description: |
        Controls bot behaviour for this conversation.
        `auto` — bot replies to every message.
        `off` — bot silent; human attends.
        `idle_takeover` — bot takes over after N minutes of inactivity.
      tags: [Bot]
      parameters:
        - $ref: '#/components/parameters/TenantSlug'
        - $ref: '#/components/parameters/ResourceID'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [mode]
              properties:
                mode:
                  type: string
                  enum: [auto, off, idle_takeover]
      responses:
        '204':
          description: Updated
        '400':
          $ref: '#/components/responses/BadRequest'
        '404':
          $ref: '#/components/responses/NotFound'

  # ─── Messages ──────────────────────────────────────────────────────────────

  /{slug}/api/v1/conversations/{conv_id}/messages:
    get:
      summary: List messages
      description: |
        Returns messages for a conversation, ordered newest-first (cursor-paginated).
        Default limit 50, max 200.
      tags: [Messages]
      parameters:
        - $ref: '#/components/parameters/TenantSlug'
        - name: conv_id
          in: path
          required: true
          schema: { type: string, format: uuid }
          description: Conversation UUID
        - name: limit
          in: query
          schema: { type: integer, default: 50, maximum: 200 }
        - name: before
          in: query
          schema: { type: string, format: date-time }
          description: Return messages older than this timestamp
      responses:
        '200':
          description: Message list (newest first)
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/Message'
        '404':
          $ref: '#/components/responses/NotFound'
    post:
      summary: Send text message
      description: |
        Sends a plain-text message from the attendant (or API key with scope
        `messages:send`) to the contact.
      tags: [Messages]
      parameters:
        - $ref: '#/components/parameters/TenantSlug'
        - name: conv_id
          in: path
          required: true
          schema: { type: string, format: uuid }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [content]
              properties:
                content: { type: string, description: "Plain text body" }
      responses:
        '201':
          description: Message enqueued
          content:
            application/json:
              schema:
                type: object
                properties:
                  id: { type: string, format: uuid }
        '400':
          $ref: '#/components/responses/BadRequest'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'

  /{slug}/api/v1/conversations/{conv_id}/messages/media:
    post:
      summary: Send media message
      description: |
        Multipart upload (`multipart/form-data`). Fields:
        - `file` (required) — binary blob, max 50 MB
        - `caption` (optional) — text caption

        The server detects MIME type and rejects if the declared Content-Type
        family mismatches the detected bytes.
      tags: [Messages]
      parameters:
        - $ref: '#/components/parameters/TenantSlug'
        - name: conv_id
          in: path
          required: true
          schema: { type: string, format: uuid }
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required: [file]
              properties:
                file:
                  type: string
                  format: binary
                  description: Media file (image, audio, video, document). Max 50 MB.
                caption:
                  type: string
                  description: Optional text caption
            encoding:
              file:
                contentType: image/*, audio/*, video/*, application/pdf, application/octet-stream
      responses:
        '201':
          description: Media message enqueued
          content:
            application/json:
              schema:
                type: object
                properties:
                  id: { type: string, format: uuid, description: Message ID }
                  media_id: { type: string, format: uuid, description: Stored media blob ID }
        '400':
          $ref: '#/components/responses/BadRequest'
        '403':
          $ref: '#/components/responses/Forbidden'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'

  # ─── Media ─────────────────────────────────────────────────────────────────

  /{slug}/api/v1/media/{id}:
    get:
      summary: Download media blob
      description: |
        Streams the raw media bytes with the original Content-Type.
        Tenant isolation enforced: blob must belong to the caller's tenant.
        Long cache: `Cache-Control: private, max-age=31536000, immutable`.
      tags: [Media]
      parameters:
        - $ref: '#/components/parameters/TenantSlug'
        - name: id
          in: path
          required: true
          schema: { type: string, format: uuid }
          description: Media blob UUID
      responses:
        '200':
          description: Raw media bytes
          content:
            image/jpeg:
              schema: { type: string, format: binary }
            image/png:
              schema: { type: string, format: binary }
            audio/ogg:
              schema: { type: string, format: binary }
            video/mp4:
              schema: { type: string, format: binary }
            application/octet-stream:
              schema: { type: string, format: binary }
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'

  # ─── Webhooks ──────────────────────────────────────────────────────────────

  /{slug}/api/v1/webhooks:
    get:
      summary: List webhooks
      description: Returns all webhooks. Secret is masked (first 8 chars + ***). Admin only.
      tags: [Webhooks]
      parameters:
        - $ref: '#/components/parameters/TenantSlug'
      responses:
        '200':
          description: Webhook list
          content:
            application/json:
              schema:
                type: object
                properties:
                  webhooks:
                    type: array
                    items:
                      $ref: '#/components/schemas/Webhook'
    post:
      summary: Create webhook
      description: |
        Registers a new outgoing webhook. The secret is returned **once** — store it
        to verify HMAC-SHA256 signatures on deliveries. Admin only.

        `event_kinds` is optional; omit or leave empty to receive all events.
        Supported events: `message.inbound`, `message.outbound`, `conversation.created`,
        `conversation.assigned`, `conversation.closed`.
      tags: [Webhooks]
      parameters:
        - $ref: '#/components/parameters/TenantSlug'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateWebhookRequest'
            example:
              url: https://n8n.example.com/webhook/whatsapp
              event_kinds: ["message.inbound", "conversation.assigned"]
      responses:
        '201':
          description: Webhook created
          content:
            application/json:
              schema:
                type: object
                properties:
                  id: { type: string, format: uuid }
                  secret: { type: string, description: HMAC signing secret — shown once }

  /{slug}/api/v1/webhooks/{id}:
    delete:
      summary: Delete webhook
      description: Removes the webhook subscription. Admin only.
      tags: [Webhooks]
      parameters:
        - $ref: '#/components/parameters/TenantSlug'
        - $ref: '#/components/parameters/ResourceID'
      responses:
        '200':
          description: Deleted
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, example: ok }
        '404':
          $ref: '#/components/responses/NotFound'

  /{slug}/api/v1/webhooks/{id}/rotate-secret:
    post:
      summary: Rotate webhook secret
      description: Generates a new HMAC secret. Old secret is immediately invalidated. Admin only.
      tags: [Webhooks]
      parameters:
        - $ref: '#/components/parameters/TenantSlug'
        - $ref: '#/components/parameters/ResourceID'
      responses:
        '200':
          description: New secret
          content:
            application/json:
              schema:
                type: object
                properties:
                  secret: { type: string }
        '404':
          $ref: '#/components/responses/NotFound'

  # ─── AI Agents ─────────────────────────────────────────────────────────────

  /{slug}/api/v1/ai-agents/providers:
    get:
      summary: List provider registry
      description: Returns the static list of supported LLM providers. Roles: admin.
      tags: [AI Agents]
      parameters:
        - $ref: '#/components/parameters/TenantSlug'
      responses:
        '200':
          description: Provider list
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/ProviderMeta'

  /{slug}/api/v1/ai-agents:
    get:
      summary: List tenant AI agents
      description: Returns `{tenant_default, session_overrides}`. Roles: admin.
      tags: [AI Agents]
      parameters:
        - $ref: '#/components/parameters/TenantSlug'
      responses:
        '200':
          description: AI agent list
          content:
            application/json:
              schema:
                type: object
                properties:
                  tenant_default:
                    $ref: '#/components/schemas/AIAgent'
                    nullable: true
                  session_overrides:
                    type: array
                    items:
                      $ref: '#/components/schemas/AIAgent'
    post:
      summary: Create or update AI agent
      description: |
        Upserts an agent row (conflict on tenant+session). Runs a live credential
        test before persisting — returns 422 if the provider rejects the key.
        Roles: admin.
      tags: [AI Agents]
      parameters:
        - $ref: '#/components/parameters/TenantSlug'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpsertAgentRequest'
      responses:
        '201':
          description: Agent saved
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AIAgent'
        '400':
          $ref: '#/components/responses/BadRequest'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'

  /{slug}/api/v1/ai-agents/{id}:
    get:
      summary: Get AI agent
      tags: [AI Agents]
      parameters:
        - $ref: '#/components/parameters/TenantSlug'
        - $ref: '#/components/parameters/ResourceID'
      responses:
        '200':
          description: AI agent
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AIAgent'
        '404':
          $ref: '#/components/responses/NotFound'
    delete:
      summary: Delete AI agent
      tags: [AI Agents]
      parameters:
        - $ref: '#/components/parameters/TenantSlug'
        - $ref: '#/components/parameters/ResourceID'
      responses:
        '204':
          description: Deleted
        '404':
          $ref: '#/components/responses/NotFound'

  /{slug}/api/v1/ai-agents/{id}/test:
    post:
      summary: Test AI agent credentials
      description: Sends a tiny completion ("Ping") to verify provider credentials. Roles: admin.
      tags: [AI Agents]
      parameters:
        - $ref: '#/components/parameters/TenantSlug'
        - $ref: '#/components/parameters/ResourceID'
      responses:
        '200':
          description: Test result
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TestAgentResponse'
        '502':
          description: Provider rejected the request
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TestAgentResponse'

  # ─── Risk Words ─────────────────────────────────────────────────────────────

  /{slug}/api/v1/risk-words:
    get:
      summary: List risk words
      description: Returns all compliance risk words for the tenant. Roles: admin, gerente.
      tags: [Risk Words]
      parameters:
        - $ref: '#/components/parameters/TenantSlug'
      responses:
        '200':
          description: Risk word list
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/RiskWord'
    post:
      summary: Create risk word
      tags: [Risk Words]
      parameters:
        - $ref: '#/components/parameters/TenantSlug'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [word, severity, action]
              properties:
                word: { type: string }
                severity: { type: string, enum: [low, medium, high] }
                action: { type: string, enum: [handoff, alert, block] }
      responses:
        '201':
          description: Created
          content:
            application/json:
              schema:
                type: object
                properties:
                  id: { type: string, format: uuid }
        '422':
          $ref: '#/components/responses/UnprocessableEntity'

  /{slug}/api/v1/risk-words/{id}:
    delete:
      summary: Delete risk word
      tags: [Risk Words]
      parameters:
        - $ref: '#/components/parameters/TenantSlug'
        - $ref: '#/components/parameters/ResourceID'
      responses:
        '204':
          description: Deleted
        '404':
          $ref: '#/components/responses/NotFound'

# ─── Superadmin Panel (Phase F) ─────────────────────────────────────────────

  /admin/api/v1/auth/login:
    post:
      tags: [Superadmin]
      summary: Superadmin login
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [email, password]
              properties:
                email: { type: string, format: email }
                password: { type: string, minLength: 8 }
      responses:
        '200':
          description: Login successful
          content:
            application/json:
              schema: { $ref: '#/components/schemas/SuperadminAuthResponse' }
        '401': { description: Credenciais inválidas }
        '429': { description: Rate limit excedido }

  /admin/api/v1/auth/refresh:
    post:
      tags: [Superadmin]
      summary: Rotate superadmin refresh token
      security: []
      responses:
        '200':
          content:
            application/json:
              schema: { $ref: '#/components/schemas/SuperadminAuthResponse' }
        '401': { description: Token inválido }

  /admin/api/v1/auth/logout:
    post:
      tags: [Superadmin]
      summary: Logout (revoke refresh token)
      security: []
      responses:
        '204': { description: Logged out }

  /admin/api/v1/me:
    get:
      tags: [Superadmin]
      summary: Get current superadmin profile
      responses:
        '200':
          content:
            application/json:
              schema: { $ref: '#/components/schemas/SuperadminUser' }
        '401': { description: Unauthorized }

  /admin/api/v1/tenants:
    get:
      tags: [Superadmin]
      summary: List all tenants with stats
      responses:
        '200':
          content:
            application/json:
              schema:
                type: array
                items: { $ref: '#/components/schemas/TenantWithStats' }
    post:
      tags: [Superadmin]
      summary: Create tenant + first admin user
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/CreateTenantRequest' }
      responses:
        '201':
          content:
            application/json:
              schema: { $ref: '#/components/schemas/CreateTenantResponse' }
        '400': { description: Slug inválido ou já em uso }

  /admin/api/v1/tenants/{slug}:
    patch:
      tags: [Superadmin]
      summary: Suspend or reactivate a tenant
      parameters:
        - name: slug
          in: path
          required: true
          schema: { type: string }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [status]
              properties:
                status: { type: string, enum: [active, suspended] }
      responses:
        '200': { description: Status updated }
        '404': { description: Tenant not found }

  /admin/api/v1/tenants/{slug}/impersonate:
    post:
      tags: [Superadmin]
      summary: Get a short-lived JWT for a tenant admin (30 min)
      parameters:
        - name: slug
          in: path
          required: true
          schema: { type: string }
      responses:
        '200':
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ImpersonateResponse' }
        '404': { description: Tenant or admin user not found }

  /admin/api/v1/superadmins:
    get:
      tags: [Superadmin]
      summary: List all superadmin users
      responses:
        '200':
          content:
            application/json:
              schema:
                type: array
                items: { $ref: '#/components/schemas/SuperadminUser' }
    post:
      tags: [Superadmin]
      summary: Create a new superadmin user
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [email, display_name, password]
              properties:
                email: { type: string, format: email }
                display_name: { type: string }
                password: { type: string, minLength: 8 }
      responses:
        '201': { description: Superadmin created }

  /admin/api/v1/superadmins/{id}/status:
    patch:
      tags: [Superadmin]
      summary: Enable or disable a superadmin user
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string, format: uuid }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [status]
              properties:
                status: { type: string, enum: [active, disabled] }
      responses:
        '200': { description: Status updated }
        '403': { description: Cannot disable own account }

  /admin/api/v1/metrics:
    get:
      tags: [Superadmin]
      summary: Global platform metrics (30s cache)
      responses:
        '200':
          content:
            application/json:
              schema: { $ref: '#/components/schemas/GlobalMetrics' }

  /admin/api/v1/audit:
    get:
      tags: [Superadmin]
      summary: Cross-tenant audit log
      parameters:
        - name: tenant_id
          in: query
          schema: { type: string, format: uuid }
        - name: superadmin_id
          in: query
          schema: { type: string, format: uuid }
        - name: action
          in: query
          schema: { type: string }
        - name: limit
          in: query
          schema: { type: integer, maximum: 500, default: 200 }
      responses:
        '200':
          content:
            application/json:
              schema:
                type: array
                items: { $ref: '#/components/schemas/AuditEntry' }

  # ─── Quick Replies ─────────────────────────────────────────────────────────

  /{slug}/api/v1/quick-replies:
    get:
      summary: List quick replies
      tags: [Quick Replies]
      parameters:
        - { name: slug, in: path, required: true, schema: { type: string } }
      responses:
        '200':
          description: Array of quick replies
          content:
            application/json:
              schema:
                type: array
                items: { $ref: '#/components/schemas/QuickReply' }
    post:
      summary: Create quick reply (admin, gerente)
      tags: [Quick Replies]
      parameters:
        - { name: slug, in: path, required: true, schema: { type: string } }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [title, body]
              properties:
                title: { type: string }
                shortcut: { type: string, nullable: true, description: "e.g. /bomdia" }
                body: { type: string }
                category: { type: string, nullable: true }
      responses:
        '201':
          description: Created quick reply
          content:
            application/json:
              schema: { $ref: '#/components/schemas/QuickReply' }

  /{slug}/api/v1/quick-replies/{id}:
    get:
      summary: Get quick reply by ID
      tags: [Quick Replies]
      parameters:
        - { name: slug, in: path, required: true, schema: { type: string } }
        - { name: id, in: path, required: true, schema: { type: string, format: uuid } }
      responses:
        '200':
          content:
            application/json:
              schema: { $ref: '#/components/schemas/QuickReply' }
    patch:
      summary: Update quick reply (admin, gerente)
      tags: [Quick Replies]
      parameters:
        - { name: slug, in: path, required: true, schema: { type: string } }
        - { name: id, in: path, required: true, schema: { type: string, format: uuid } }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                title: { type: string }
                shortcut: { type: string, nullable: true }
                body: { type: string }
                category: { type: string, nullable: true }
      responses:
        '200':
          content:
            application/json:
              schema: { $ref: '#/components/schemas/QuickReply' }
    delete:
      summary: Delete quick reply (admin, gerente)
      tags: [Quick Replies]
      parameters:
        - { name: slug, in: path, required: true, schema: { type: string } }
        - { name: id, in: path, required: true, schema: { type: string, format: uuid } }
      responses:
        '204': { description: Deleted }

  # ─── Scheduled Messages ────────────────────────────────────────────────────

  /{slug}/api/v1/scheduled-messages:
    get:
      summary: List scheduled messages
      tags: [Scheduled Messages]
      parameters:
        - { name: slug, in: path, required: true, schema: { type: string } }
        - { name: conversation_id, in: query, schema: { type: string, format: uuid } }
        - { name: status, in: query, schema: { type: string, enum: [pending, sent, cancelled, failed] } }
      responses:
        '200':
          description: Array of scheduled messages
          content:
            application/json:
              schema:
                type: array
                items: { $ref: '#/components/schemas/ScheduledMessage' }
    post:
      summary: Create scheduled message
      tags: [Scheduled Messages]
      parameters:
        - { name: slug, in: path, required: true, schema: { type: string } }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [conversation_id, message, scheduled_for]
              properties:
                conversation_id: { type: string, format: uuid }
                message: { type: string }
                scheduled_for: { type: string, format: date-time, description: RFC3339 future datetime }
      responses:
        '201':
          description: Created
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ScheduledMessage' }

  /{slug}/api/v1/scheduled-messages/{id}:
    delete:
      summary: Cancel a pending scheduled message
      tags: [Scheduled Messages]
      parameters:
        - { name: slug, in: path, required: true, schema: { type: string } }
        - { name: id, in: path, required: true, schema: { type: string, format: uuid } }
      responses:
        '204': { description: Cancelled }
        '404': { description: Not found or not cancellable }

  # ─── Campaigns ─────────────────────────────────────────────────────────────

  /{slug}/api/v1/campaigns:
    get:
      summary: List campaigns (admin)
      tags: [Campaigns]
      parameters:
        - { name: slug, in: path, required: true, schema: { type: string } }
      responses:
        '200':
          description: Array of campaigns with aggregate stats
          content:
            application/json:
              schema:
                type: array
                items: { $ref: '#/components/schemas/Campaign' }
    post:
      summary: Create campaign draft (admin)
      tags: [Campaigns]
      parameters:
        - { name: slug, in: path, required: true, schema: { type: string } }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [session_id, title, body, target_jids]
              properties:
                session_id: { type: string, format: uuid }
                title: { type: string }
                body: { type: string }
                media_id: { type: string, format: uuid, nullable: true }
                target_jids:
                  type: array
                  items: { type: string }
                  description: "List of JIDs e.g. 5511999990000@s.whatsapp.net"
                scheduled_for: { type: string, format: date-time, nullable: true }
                rate_per_min: { type: integer, default: 5, description: Max dispatches per minute (anti-ban) }
      responses:
        '201':
          description: Campaign created (draft status)
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Campaign' }

  /{slug}/api/v1/campaigns/{id}:
    get:
      summary: Get campaign detail with stats (admin)
      tags: [Campaigns]
      parameters:
        - { name: slug, in: path, required: true, schema: { type: string } }
        - { name: id, in: path, required: true, schema: { type: string, format: uuid } }
      responses:
        '200':
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Campaign' }
    delete:
      summary: Delete draft campaign (admin)
      tags: [Campaigns]
      parameters:
        - { name: slug, in: path, required: true, schema: { type: string } }
        - { name: id, in: path, required: true, schema: { type: string, format: uuid } }
      responses:
        '204': { description: Deleted }
        '404': { description: Not found or not in draft status }

  /{slug}/api/v1/campaigns/{id}/start:
    post:
      summary: Start or schedule a campaign (admin)
      description: |
        If the campaign has a future `scheduled_for`, status transitions to `scheduled`.
        Otherwise transitions immediately to `running`.
      tags: [Campaigns]
      parameters:
        - { name: slug, in: path, required: true, schema: { type: string } }
        - { name: id, in: path, required: true, schema: { type: string, format: uuid } }
      responses:
        '200':
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Campaign' }

  /{slug}/api/v1/campaigns/{id}/cancel:
    post:
      summary: Cancel a campaign (admin)
      tags: [Campaigns]
      parameters:
        - { name: slug, in: path, required: true, schema: { type: string } }
        - { name: id, in: path, required: true, schema: { type: string, format: uuid } }
      responses:
        '204': { description: Cancelled }

  /{slug}/api/v1/campaigns/{id}/dispatches:
    get:
      summary: List campaign dispatch records (admin)
      tags: [Campaigns]
      parameters:
        - { name: slug, in: path, required: true, schema: { type: string } }
        - { name: id, in: path, required: true, schema: { type: string, format: uuid } }
        - { name: limit, in: query, schema: { type: integer, default: 100 } }
      responses:
        '200':
          description: Array of dispatch records
          content:
            application/json:
              schema:
                type: array
                items: { $ref: '#/components/schemas/CampaignDispatch' }

  # ─── Phase D: Audit ──────────────────────────────────────────────────────────

  /{slug}/api/v1/audit:
    get:
      summary: List audit log entries (admin, gerente)
      tags: [Audit]
      security:
        - BearerAuth: []
        - ApiKeyAuth: []
      parameters:
        - { name: slug, in: path, required: true, schema: { type: string } }
        - { name: action, in: query, schema: { type: string }, description: Filter by action (e.g. user.login) }
        - { name: limit, in: query, schema: { type: integer, default: 50 } }
      responses:
        '200':
          description: Array of audit log entries
          content:
            application/json:
              schema:
                type: array
                items: { $ref: '#/components/schemas/AuditEntry' }

  # ─── Phase D: Bot Approvals ───────────────────────────────────────────────

  # ─── Phase E: Workflows ──────────────────────────────────────────────────

  /{slug}/api/v1/workflows/registry:
    get:
      summary: Get node type registry (palette for the visual builder)
      tags: [Workflows]
      security:
        - BearerAuth: []
        - ApiKeyAuth: []
      parameters:
        - { name: slug, in: path, required: true, schema: { type: string } }
      responses:
        '200':
          description: Array of node type descriptors
          content:
            application/json:
              schema:
                type: array
                items: { $ref: '#/components/schemas/WorkflowNodeType' }

  /{slug}/api/v1/workflows:
    get:
      summary: List workflows for the tenant
      tags: [Workflows]
      security:
        - BearerAuth: []
        - ApiKeyAuth: []
      parameters:
        - { name: slug, in: path, required: true, schema: { type: string } }
      responses:
        '200':
          description: Array of workflows
          content:
            application/json:
              schema:
                type: array
                items: { $ref: '#/components/schemas/Workflow' }
    post:
      summary: Create a workflow (is_active defaults to false)
      tags: [Workflows]
      security:
        - BearerAuth: []
        - ApiKeyAuth: []
      parameters:
        - { name: slug, in: path, required: true, schema: { type: string } }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name, trigger_kind]
              properties:
                name: { type: string }
                description: { type: string, nullable: true }
                graph: { type: object, description: "{ nodes: [...], edges: [...] }" }
                trigger_kind: { type: string, description: Entry node kind }
                trigger_config: { type: object }
      responses:
        '201':
          description: Created workflow
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Workflow' }

  /{slug}/api/v1/workflows/{id}:
    get:
      summary: Get a workflow by ID
      tags: [Workflows]
      security:
        - BearerAuth: []
        - ApiKeyAuth: []
      parameters:
        - { name: slug, in: path, required: true, schema: { type: string } }
        - { name: id, in: path, required: true, schema: { type: string, format: uuid } }
      responses:
        '200':
          description: Workflow
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Workflow' }
        '404': { description: Not found }
    patch:
      summary: Update a workflow
      tags: [Workflows]
      security:
        - BearerAuth: []
        - ApiKeyAuth: []
      parameters:
        - { name: slug, in: path, required: true, schema: { type: string } }
        - { name: id, in: path, required: true, schema: { type: string, format: uuid } }
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                name: { type: string }
                description: { type: string, nullable: true }
                graph: { type: object }
                trigger_kind: { type: string }
                trigger_config: { type: object }
      responses:
        '200':
          description: Updated workflow
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Workflow' }
        '404': { description: Not found }
    delete:
      summary: Delete a workflow
      tags: [Workflows]
      security:
        - BearerAuth: []
        - ApiKeyAuth: []
      parameters:
        - { name: slug, in: path, required: true, schema: { type: string } }
        - { name: id, in: path, required: true, schema: { type: string, format: uuid } }
      responses:
        '204': { description: Deleted }
        '404': { description: Not found }

  /{slug}/api/v1/workflows/{id}/activate:
    post:
      summary: Activate a workflow
      tags: [Workflows]
      security:
        - BearerAuth: []
        - ApiKeyAuth: []
      parameters:
        - { name: slug, in: path, required: true, schema: { type: string } }
        - { name: id, in: path, required: true, schema: { type: string, format: uuid } }
      responses:
        '200': { description: Activated }
        '404': { description: Not found }

  /{slug}/api/v1/workflows/{id}/deactivate:
    post:
      summary: Deactivate a workflow
      tags: [Workflows]
      security:
        - BearerAuth: []
        - ApiKeyAuth: []
      parameters:
        - { name: slug, in: path, required: true, schema: { type: string } }
        - { name: id, in: path, required: true, schema: { type: string, format: uuid } }
      responses:
        '200': { description: Deactivated }
        '404': { description: Not found }

  /{slug}/api/v1/workflows/{id}/runs:
    get:
      summary: List execution history for a workflow
      tags: [Workflows]
      security:
        - BearerAuth: []
        - ApiKeyAuth: []
      parameters:
        - { name: slug, in: path, required: true, schema: { type: string } }
        - { name: id, in: path, required: true, schema: { type: string, format: uuid } }
        - { name: limit, in: query, schema: { type: integer, default: 50 } }
      responses:
        '200':
          description: Array of workflow runs
          content:
            application/json:
              schema:
                type: array
                items: { $ref: '#/components/schemas/WorkflowRun' }

  # ─── Phase D: Training Feedback ───────────────────────────────────────────

  /{slug}/api/v1/messages/{message_id}/feedback:
    post:
      summary: Submit thumbs-up/down feedback on a bot message
      tags: [Training]
      security:
        - BearerAuth: []
        - ApiKeyAuth: []
      parameters:
        - { name: slug, in: path, required: true, schema: { type: string } }
        - { name: message_id, in: path, required: true, schema: { type: string, format: uuid } }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [rating]
              properties:
                rating:
                  type: string
                  enum: [up, down]
                note: { type: string }
      responses:
        '204': { description: Feedback recorded }
        '400': { description: Invalid rating }

  /{slug}/api/v1/training/feedback:
    get:
      summary: List training feedback entries (admin, gerente)
      tags: [Training]
      security:
        - BearerAuth: []
        - ApiKeyAuth: []
      parameters:
        - { name: slug, in: path, required: true, schema: { type: string } }
        - { name: agent_id, in: query, schema: { type: string, format: uuid } }
        - { name: rating, in: query, schema: { type: string, enum: [up, down] } }
        - { name: since, in: query, schema: { type: string, format: date-time } }
        - { name: limit, in: query, schema: { type: integer, default: 50 } }
      responses:
        '200':
          description: Array of feedback entries
          content:
            application/json:
              schema:
                type: array
                items: { $ref: '#/components/schemas/TrainingFeedbackEntry' }

  /{slug}/api/v1/training/feedback/{id}:
    delete:
      summary: Delete own feedback entry
      tags: [Training]
      security:
        - BearerAuth: []
        - ApiKeyAuth: []
      parameters:
        - { name: slug, in: path, required: true, schema: { type: string } }
        - { name: id, in: path, required: true, schema: { type: string, format: uuid } }
      responses:
        '204': { description: Deleted }
        '403': { description: Not owner }
        '404': { description: Not found }

# ─── Components ──────────────────────────────────────────────────────────────

components:

  securitySchemes:
    BearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: JWT for attendant users. Issued by POST /{slug}/api/v1/auth/login.
    ApiKeyAuth:
      type: http
      scheme: bearer
      bearerFormat: wsk_*
      description: |
        API key for integrations. Format `wsk_<hex>`. The `wsk_` prefix distinguishes
        keys from JWTs at the auth middleware. Scope-based; required scopes are noted
        per endpoint.

  parameters:
    TenantSlug:
      name: slug
      in: path
      required: true
      schema:
        type: string
        pattern: '^[a-z0-9][a-z0-9-]{2,30}$'
      description: Tenant slug (from URL path)
    ResourceID:
      name: id
      in: path
      required: true
      schema:
        type: string
        format: uuid
      description: Resource UUID

  responses:
    BadRequest:
      description: Bad request — malformed JSON or missing required field
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    Unauthorized:
      description: Authentication required or token invalid/expired
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    Forbidden:
      description: Authenticated but insufficient role or ownership
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    NotFound:
      description: Resource not found
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    TooManyRequests:
      description: Rate limit exceeded
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    UnprocessableEntity:
      description: Validation error (e.g. MIME mismatch, invalid enum)
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'

  schemas:

    Error:
      type: object
      properties:
        error:
          type: string
          description: Human-readable error message
        code:
          type: string
          description: Machine-readable error code (optional)
      example:
        error: "invalid credentials"

    LoginRequest:
      type: object
      required: [email, password]
      properties:
        email:
          type: string
          format: email
        password:
          type: string
          format: password

    AcceptInviteRequest:
      type: object
      required: [invite_token, password]
      properties:
        invite_token:
          type: string
          description: 32-char hex token from the invite email
        password:
          type: string
          format: password
          minLength: 8

    AuthResponse:
      type: object
      properties:
        access_token:
          type: string
          description: JWT — include as `Authorization: Bearer <token>`
        user:
          type: object
          properties:
            id: { type: string, format: uuid }
            display_name: { type: string }
            role: { type: string, enum: [admin, gerente, atendente] }

    User:
      type: object
      properties:
        id: { type: string, format: uuid }
        email: { type: string, format: email }
        display_name: { type: string }
        role: { type: string, enum: [admin, gerente, atendente] }
        status: { type: string, enum: [active, invited, disabled] }
        last_login_at: { type: string, format: date-time, nullable: true }
        created_at: { type: string, format: date-time }

    InviteUserRequest:
      type: object
      required: [email, display_name, role]
      properties:
        email: { type: string, format: email }
        display_name: { type: string }
        role: { type: string, enum: [admin, gerente, atendente] }

    APIKey:
      type: object
      properties:
        id: { type: string, format: uuid }
        name: { type: string }
        prefix: { type: string, description: "First chars of key, e.g. wsk_a1b2" }
        scopes:
          type: array
          items: { type: string }
        created_at: { type: string, format: date-time }
        revoked_at: { type: string, format: date-time, nullable: true }

    CreateAPIKeyRequest:
      type: object
      required: [name]
      properties:
        name: { type: string }
        scopes:
          type: array
          items:
            type: string
            enum: [messages:send, conversations:read, webhooks:manage]

    Session:
      type: object
      properties:
        id: { type: string, format: uuid }
        tenant_id: { type: string, format: uuid }
        name: { type: string }
        user_agent:
          type: string
          description: "WhatsApp client fingerprint, e.g. Chrome, Firefox, Safari"
        device_jid:
          type: string
          description: WhatsApp JID once paired (empty before first pair)
        status:
          type: string
          enum: [pending_qr, connecting, connected, disconnected, logged_out]
        auto_reply: { type: boolean }
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }

    CreateSessionRequest:
      type: object
      required: [name, user_agent]
      properties:
        name: { type: string, description: "Human label, e.g. 'Loja Centro'" }
        user_agent:
          type: string
          description: "Must be one of the valid display strings (Chrome, Firefox, Safari, etc.)"

    Contact:
      type: object
      properties:
        id: { type: string, format: uuid }
        tenant_id: { type: string, format: uuid }
        session_id: { type: string, format: uuid }
        jid: { type: string, description: "WhatsApp JID, e.g. 5511999990000@s.whatsapp.net" }
        push_name: { type: string, description: Name set by the contact in WA }
        display_name: { type: string, description: Override name set by the team }
        phone: { type: string }
        profile_picture_mime: { type: string }
        profile_picture_fetched_at: { type: string, format: date-time, nullable: true }
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }

    Conversation:
      type: object
      properties:
        id: { type: string, format: uuid }
        tenant_id: { type: string, format: uuid }
        session_id: { type: string, format: uuid }
        contact_id: { type: string, format: uuid }
        status:
          type: string
          enum: [queued, open, closed, bot]
        assigned_user_id: { type: string, format: uuid, nullable: true }
        bot_mode:
          type: string
          enum: [auto, off, idle_takeover]
        last_message_at: { type: string, format: date-time, nullable: true }
        created_at: { type: string, format: date-time }

    ConversationPreview:
      allOf:
        - $ref: '#/components/schemas/Conversation'
        - type: object
          properties:
            last_message_preview: { type: string, nullable: true }
            contact_push_name: { type: string }
            contact_phone: { type: string }
            unread_count: { type: integer, default: 0 }

    Message:
      type: object
      properties:
        id: { type: string, format: uuid }
        direction:
          type: string
          enum: [inbound, outbound]
        author_type:
          type: string
          enum: [contact, user, api, bot]
        content_type:
          type: string
          enum: [text, image, audio, video, document]
        text_body: { type: string, nullable: true }
        attendant_prefix: { type: string, nullable: true }
        media_id: { type: string, format: uuid, nullable: true }
        media_mime: { type: string, nullable: true }
        media_size: { type: integer, format: int64, nullable: true }
        transcription: { type: string, nullable: true, description: Whisper transcription for audio }
        status:
          type: string
          enum: [pending, sent, delivered, read, failed]
        created_at: { type: string, format: date-time }

    ProviderMeta:
      type: object
      properties:
        id: { type: string, description: "e.g. openai, deepseek, groq" }
        display_name: { type: string }
        default_base: { type: string, format: uri }
        default_model: { type: string }
        custom: { type: boolean, description: Allows arbitrary api_base if true }
        model_suggestions:
          type: array
          items: { type: string }

    GoldenRule:
      type: object
      required: [id, rule, active]
      properties:
        id: { type: string }
        rule: { type: string }
        active: { type: boolean }

    GoldenRules:
      type: object
      properties:
        always:
          type: array
          items:
            $ref: '#/components/schemas/GoldenRule'
        never:
          type: array
          items:
            $ref: '#/components/schemas/GoldenRule'

    AIAgent:
      type: object
      properties:
        id: { type: string, format: uuid }
        tenant_id: { type: string, format: uuid }
        session_id: { type: string, format: uuid, nullable: true }
        name: { type: string }
        is_active: { type: boolean }
        provider: { type: string, enum: [openai, deepseek, groq, anthropic, gemini, xai, mistral, openrouter, together, custom] }
        api_base: { type: string, format: uri }
        api_key: { type: string }
        model: { type: string }
        temperature: { type: number, format: float, minimum: 0, maximum: 2 }
        max_tokens: { type: integer }
        system_prompt: { type: string }
        golden_rules:
          $ref: '#/components/schemas/GoldenRules'
        handoff_keywords:
          type: array
          items: { type: string }
        knowledge_scope:
          type: array
          items: { type: string, format: uuid }
        confidence_threshold: { type: number, format: float }
        last_tested_at: { type: string, format: date-time, nullable: true }
        last_test_status: { type: string, enum: [ok, failed, ""] }
        last_test_error: { type: string }
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }

    UpsertAgentRequest:
      type: object
      required: [provider, api_key, model]
      properties:
        session_id: { type: string, format: uuid, nullable: true }
        name: { type: string }
        provider: { type: string, enum: [openai, deepseek, groq, anthropic, gemini, xai, mistral, openrouter, together, custom] }
        api_base: { type: string, format: uri, description: Required for custom provider }
        api_key: { type: string }
        model: { type: string }
        temperature: { type: number, format: float, default: 0.3 }
        max_tokens: { type: integer, default: 1024 }
        system_prompt: { type: string }
        golden_rules:
          $ref: '#/components/schemas/GoldenRules'
        handoff_keywords:
          type: array
          items: { type: string }

    TestAgentResponse:
      type: object
      properties:
        status: { type: string, enum: [ok, failed] }
        latency_ms: { type: integer }
        sample: { type: string, nullable: true, description: First 80 chars of the reply }
        error: { type: string, nullable: true }

    RiskWord:
      type: object
      properties:
        id: { type: string, format: uuid }
        word: { type: string }
        severity: { type: string, enum: [low, medium, high] }
        action: { type: string, enum: [handoff, alert, block] }
        created_at: { type: string, format: date-time }

    Webhook:
      type: object
      properties:
        id: { type: string, format: uuid }
        url: { type: string, format: uri }
        secret: { type: string, description: "Masked: first 8 chars + ***" }
        event_kinds:
          type: array
          items: { type: string }
        active: { type: boolean }
        failure_count: { type: integer }
        last_delivery_at: { type: string, format: date-time, nullable: true }
        created_at: { type: string, format: date-time }

    CreateWebhookRequest:
      type: object
      required: [url]
      properties:
        url:
          type: string
          format: uri
        event_kinds:
          type: array
          items:
            type: string
            enum:
              - message.inbound
              - message.outbound
              - conversation.created
              - conversation.assigned
              - conversation.closed
          description: Leave empty to receive all events

    # ── Superadmin schemas (Phase F) ──────────────────────────────────────────

    SuperadminAuthResponse:
      type: object
      properties:
        access_token: { type: string }
        user: { $ref: '#/components/schemas/SuperadminUser' }

    SuperadminUser:
      type: object
      properties:
        id: { type: string, format: uuid }
        email: { type: string }
        display_name: { type: string }
        status: { type: string, enum: [active, disabled] }
        last_login_at: { type: string, format: date-time, nullable: true }
        created_at: { type: string, format: date-time }

    TenantWithStats:
      type: object
      properties:
        id: { type: string, format: uuid }
        slug: { type: string }
        name: { type: string }
        status: { type: string, enum: [active, suspended] }
        user_count: { type: integer }
        session_count: { type: integer }
        last_activity: { type: string, format: date-time, nullable: true }
        created_at: { type: string, format: date-time }

    CreateTenantRequest:
      type: object
      required: [slug, name, admin_email, admin_display_name, admin_password]
      properties:
        slug: { type: string, pattern: '^[a-z0-9][a-z0-9-]{2,30}$' }
        name: { type: string }
        admin_email: { type: string, format: email }
        admin_display_name: { type: string }
        admin_password: { type: string, minLength: 8 }

    CreateTenantResponse:
      type: object
      properties:
        tenant:
          type: object
          properties:
            id: { type: string, format: uuid }
            slug: { type: string }
            name: { type: string }
        admin_user_id: { type: string, format: uuid }

    ImpersonateResponse:
      type: object
      properties:
        access_token: { type: string }
        tenant_slug: { type: string }
        expires_in: { type: integer, description: 'Seconds until expiry (1800 = 30 min)' }
        user:
          type: object
          properties:
            id: { type: string, format: uuid }
            display_name: { type: string }
            role: { type: string }

    GlobalMetrics:
      type: object
      properties:
        sessions_by_status:
          type: object
          additionalProperties: { type: integer }
        tenants_active: { type: integer }
        agents_configured: { type: integer }
        messages_today: { type: integer }
        total_active_users: { type: integer }

    AuditEntry:
      type: object
      properties:
        id: { type: integer }
        tenant_id: { type: string, format: uuid, nullable: true }
        superadmin_user_id: { type: string, format: uuid, nullable: true }
        action: { type: string }
        target_type: { type: string }
        target_id: { type: string }
        metadata: { type: object }
        ip: { type: string }
        created_at: { type: string, format: date-time }

    # ── Phase C schemas ────────────────────────────────────────────────────────

    QuickReply:
      type: object
      properties:
        id: { type: string, format: uuid }
        tenant_id: { type: string, format: uuid }
        title: { type: string }
        shortcut: { type: string, nullable: true, description: "Slash shortcut e.g. /bomdia" }
        body: { type: string }
        category: { type: string, nullable: true }
        created_by: { type: string, format: uuid, nullable: true }
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }

    ScheduledMessage:
      type: object
      properties:
        id: { type: string, format: uuid }
        tenant_id: { type: string, format: uuid }
        conversation_id: { type: string, format: uuid }
        contact_id: { type: string, format: uuid }
        session_id: { type: string, format: uuid }
        message: { type: string }
        scheduled_for: { type: string, format: date-time }
        status:
          type: string
          enum: [pending, sent, cancelled, failed]
        sent_at: { type: string, format: date-time, nullable: true }
        error_message: { type: string, nullable: true }
        created_by: { type: string, format: uuid, nullable: true }
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }

    Campaign:
      type: object
      properties:
        id: { type: string, format: uuid }
        tenant_id: { type: string, format: uuid }
        session_id: { type: string, format: uuid }
        title: { type: string }
        body: { type: string }
        media_id: { type: string, format: uuid, nullable: true }
        target_jids:
          type: array
          items: { type: string }
        scheduled_for: { type: string, format: date-time, nullable: true }
        status:
          type: string
          enum: [draft, scheduled, running, completed, cancelled, failed]
        rate_per_min: { type: integer, description: Max dispatches per minute }
        started_at: { type: string, format: date-time, nullable: true }
        completed_at: { type: string, format: date-time, nullable: true }
        created_by: { type: string, format: uuid, nullable: true }
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }
        total_dispatches: { type: integer }
        sent_dispatches: { type: integer }
        failed_dispatches: { type: integer }

    CampaignDispatch:
      type: object
      properties:
        id: { type: string, format: uuid }
        campaign_id: { type: string, format: uuid }
        jid: { type: string }
        status:
          type: string
          enum: [pending, sent, failed, skipped]
        wa_message_id: { type: string, nullable: true }
        error: { type: string, nullable: true }
        sent_at: { type: string, format: date-time, nullable: true }

    # ─── Phase D schemas ─────────────────────────────────────────────────────

    AuditEntry:
      type: object
      properties:
        id: { type: integer }
        tenant_id: { type: string, format: uuid, nullable: true }
        user_id: { type: string, format: uuid, nullable: true }
        action: { type: string, description: Dot-namespaced action (e.g. user.login) }
        target_type: { type: string, nullable: true }
        target_id: { type: string, nullable: true }
        metadata: { type: object, nullable: true }
        ip_address: { type: string, nullable: true }
        user_agent: { type: string, nullable: true }
        created_at: { type: string, format: date-time }

    # ─── Phase E: Workflow schemas ────────────────────────────────────────────

    Workflow:
      type: object
      properties:
        id: { type: string, format: uuid }
        tenant_id: { type: string, format: uuid }
        name: { type: string }
        description: { type: string, nullable: true }
        is_active: { type: boolean }
        graph: { type: object, description: "JSONB graph { nodes: [...], edges: [...] }" }
        trigger_kind: { type: string }
        trigger_config: { type: object }
        created_by: { type: string, format: uuid, nullable: true }
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }

    WorkflowNodeType:
      type: object
      properties:
        id: { type: string, description: "e.g. trigger.message_received, action.send_message" }
        display_name: { type: string }
        description: { type: string }
        icon: { type: string, description: Lucide icon name hint }
        category:
          type: string
          enum: [trigger, action]
        config_schema: { type: object, nullable: true, description: Informal schema for the node config }

    WorkflowRun:
      type: object
      properties:
        id: { type: string, format: uuid }
        tenant_id: { type: string, format: uuid }
        workflow_id: { type: string, format: uuid }
        conversation_id: { type: string, format: uuid, nullable: true }
        trigger_event: { type: string }
        trigger_payload: { type: object, nullable: true }
        status:
          type: string
          enum: [running, completed, failed, cancelled]
        steps_executed:
          type: array
          items:
            type: object
            properties:
              node_id: { type: string }
              result: { type: string }
              ts: { type: string, format: date-time }
        error_message: { type: string, nullable: true }
        started_at: { type: string, format: date-time }
        completed_at: { type: string, format: date-time, nullable: true }

    TrainingFeedbackEntry:
      type: object
      properties:
        id: { type: string, format: uuid }
        tenant_id: { type: string, format: uuid }
        message_id: { type: string, format: uuid }
        rating:
          type: string
          enum: [up, down]
        note: { type: string, nullable: true }
        created_by: { type: string, format: uuid, nullable: true }
        created_at: { type: string, format: date-time }
        conversation_id: { type: string, format: uuid, nullable: true }
        agent_id: { type: string, format: uuid, nullable: true }
        agent_name: { type: string, nullable: true }
        contact_name: { type: string, nullable: true }
