openapi: 3.1.0
info:
  title: Jelliu API
  version: 1.0.0
  description: |
    Jelliu is an AI voice and omnichannel conversation platform for outbound calling, WhatsApp,
    email and web-chat campaigns. This reference documents every tenant-facing REST endpoint under
    `/api/*` plus the public widget, Zapier and inbound webhook routes. All request and response
    bodies use `application/json` unless noted otherwise; timestamps are ISO-8601 UTC.
  contact:
    name: Jelliu Support
    email: support@jelliu.co
    url: https://jelliu.co
  license:
    name: Apache-2.0
    url: https://www.apache.org/licenses/LICENSE-2.0

servers:
  - url: https://api.jelliu.co
    description: Production
  - url: https://staging-api.jelliu.co
    description: Staging

security:
  - ClerkBearer: []
  - ApiKeyBearer: []

tags:
  - name: Calls
    description: Outbound call initiation, history and live transfer control.
  - name: Agents
    description: AI agent configuration, voice, prompts, A/B variants and tools.
  - name: Agent Tools
    description: Per-agent webhook / function tools exposed to the LLM.
  - name: Agent Workflow
    description: Per-agent conversation workflow — subagent phases and branches, synced to the voice provider.
  - name: Campaigns
    description: Outbound campaign orchestration, scheduling and state transitions.
  - name: Contacts
    description: Campaign contact lists, bulk import and CSV upload flows.
  - name: Analytics
    description: Aggregated KPIs, funnels, events and time-series metrics.
  - name: Dashboard
    description: Dashboard bundle endpoint with pre-aggregated tiles.
  - name: Conversations
    description: Unified conversation store across voice and chat channels.
  - name: Webchat
    description: Embedded web-chat messaging and session management.
  - name: Widgets
    description: Web-chat widget configuration and embed scripts.
  - name: Integrations
    description: CRM integrations, OAuth connect flow, field mapping and sync.
  - name: Zapier
    description: Zapier REST Hooks subscriptions and API key management.
  - name: Exports
    description: Asynchronous export jobs for calls, contacts, conversations and analytics.
  - name: Onboarding
    description: Workspace bootstrap flow — profile, templates, progress tracking.
  - name: Reports
    description: On-demand reports (ROI, campaign comparison, agent performance).
  - name: Legal
    description: DPA acceptance and legal agreement status.
  - name: Webhooks
    description: Outbound tenant webhooks — CRUD and delivery logs.
  - name: API Keys
    description: Developer API key lifecycle (tenant scoped `jl_` bearer tokens).
  - name: MCP Servers
    description: Model-Context-Protocol server registry and per-agent assignments.
  - name: Knowledge
    description: Per-agent knowledge base document upload and management.
  - name: Audit
    description: Admin-only audit log query API.
  - name: Compliance
    description: Consent records, GDPR erasure and country-specific compliance configs.
  - name: Billing
    description: Stripe subscription, usage metering and add-on purchases.
  - name: WhatsApp
    description: WhatsApp outbound messaging.
  - name: Email
    description: Email outbound messaging.
  - name: Inbound Webhooks
    description: Public webhook endpoints invoked by Twilio, ElevenLabs, Stripe, GitHub and CRMs.

x-tagGroups:
  - name: Calling
    tags:
      - Calls
      - WhatsApp
      - Email
      - Webchat
      - Conversations
  - name: Agents & Campaigns
    tags:
      - Agents
      - Agent Tools
      - Agent Workflow
      - Knowledge
      - Campaigns
      - Contacts
      - Widgets
  - name: Integrations
    tags:
      - Integrations
      - Zapier
      - Webhooks
      - Inbound Webhooks
  - name: Platform
    tags:
      - API Keys
      - MCP Servers
      - Onboarding
      - Legal
      - Billing
  - name: Admin
    tags:
      - Analytics
      - Dashboard
      - Reports
      - Exports
      - Audit
      - Compliance

paths:
  # ───────────────────────── Calls ─────────────────────────
  /api/calls:
    get:
      tags: [Calls]
      summary: List calls
      operationId: getCalls
      description: Returns a paginated list of calls for the current tenant, optionally filtered by campaign, agent, status or date range.
      parameters:
        - $ref: '#/components/parameters/Page'
        - $ref: '#/components/parameters/Limit'
        - name: campaignId
          in: query
          schema: { type: string, format: uuid }
        - name: agentId
          in: query
          schema: { type: string, format: uuid }
        - name: status
          in: query
          schema: { type: string }
      responses:
        '200':
          description: Paginated calls
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: '#/components/schemas/Call' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '429': { $ref: '#/components/responses/RateLimited' }
    post:
      tags: [Calls]
      summary: Initiate an outbound call
      operationId: postCalls
      description: Starts a new AI-driven outbound call. Provide either a raw `phoneNumber` (E.164) or a `contactId`; an `agentId` is always required.
      x-rate-limit:
        name: outboundLimiter
        note: Stricter limiter applied to outbound call initiation.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/InitiateCallInput' }
            example:
              phoneNumber: "+573001234567"
              agentId: "2f2d8b4d-aa6f-41e0-9d3e-4d61e1b07a11"
              campaignId: "fbb7c4c9-0e1d-4f58-a3d7-6c3a99b8d7c0"
      responses:
        '201':
          description: Call accepted and queued
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Call' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '402': { $ref: '#/components/responses/PaymentRequired' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /api/calls/{callId}:
    get:
      tags: [Calls]
      summary: Get call by id
      operationId: getCallsByCallId
      description: Retrieves a single call with transcript metadata, outcome and post-call analysis.
      parameters:
        - $ref: '#/components/parameters/CallId'
      responses:
        '200':
          description: Call record
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Call' }
        '404': { $ref: '#/components/responses/NotFound' }

  /api/calls/{callId}/hangup:
    post:
      tags: [Calls]
      summary: Hang up a live call
      operationId: postCallsByCallIdHangup
      description: Ends an active call immediately. Idempotent — calls already in a terminal state (completed, failed, no-answer, busy, canceled) return 200 without side effects.
      parameters:
        - $ref: '#/components/parameters/CallId'
      responses:
        '200': { description: Call ended (or was already terminal) }
        '404': { $ref: '#/components/responses/NotFound' }

  /api/calls/{callId}/transfer:
    post:
      tags: [Calls]
      summary: Initiate warm transfer
      operationId: postCallsByCallIdTransfer
      description: Initiates a human-agent transfer for an active call. The target is resolved via the call's agent or explicit target.
      parameters:
        - $ref: '#/components/parameters/CallId'
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                targetPhoneNumber:
                  type: string
                  description: Optional E.164 number to transfer to (overrides agent default).
                reason: { type: string, maxLength: 500 }
      responses:
        '200': { description: Transfer initiated }
        '404': { $ref: '#/components/responses/NotFound' }

  /api/calls/{callId}/transfer/complete:
    post:
      tags: [Calls]
      summary: Complete a warm transfer
      operationId: postCallsByCallIdTransferComplete
      description: Marks an in-progress transfer as successfully completed (caller handed off to human agent).
      parameters:
        - $ref: '#/components/parameters/CallId'
      responses:
        '200': { description: Transfer completed }
        '404': { $ref: '#/components/responses/NotFound' }

  /api/calls/{callId}/transfer/cancel:
    post:
      tags: [Calls]
      summary: Cancel a warm transfer
      operationId: postCallsByCallIdTransferCancel
      description: Cancels an in-progress transfer and returns control to the AI agent.
      parameters:
        - $ref: '#/components/parameters/CallId'
      responses:
        '200': { description: Transfer cancelled }
        '404': { $ref: '#/components/responses/NotFound' }

  # ───────────────────────── Agents ─────────────────────────
  /api/agents:
    get:
      tags: [Agents]
      summary: List agents
      operationId: getAgents
      description: Returns all AI agents configured for the tenant.
      responses:
        '200':
          description: Agents list
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: '#/components/schemas/Agent' }
    post:
      tags: [Agents]
      summary: Create an agent
      operationId: postAgents
      description: Creates a new AI agent with prompt, voice and behaviour configuration. Gated by the `agents` billing feature.
      x-rate-limit:
        name: agentMutationLimiter
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/CreateAgentInput' }
      responses:
        '201':
          description: Created
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Agent' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '402': { $ref: '#/components/responses/PaymentRequired' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /api/agents/{agentId}:
    get:
      tags: [Agents]
      summary: Get agent by id
      operationId: getAgentsByAgentId
      description: "Get agent by id. Returns the record for the current tenant."
      parameters:
        - $ref: '#/components/parameters/AgentId'
      responses:
        '200':
          description: Agent
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Agent' }
        '404': { $ref: '#/components/responses/NotFound' }
    patch:
      tags: [Agents]
      summary: Update an agent
      operationId: patchAgentsByAgentId
      description: Partially updates an AI agent. Only provided fields are mutated; prompts are run through an injection-pattern screener.
      x-rate-limit:
        name: agentMutationLimiter
      parameters:
        - $ref: '#/components/parameters/AgentId'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/UpdateAgentInput' }
      responses:
        '200':
          description: Updated
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Agent' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '404': { $ref: '#/components/responses/NotFound' }
    delete:
      tags: [Agents]
      summary: Delete an agent
      operationId: deleteAgentsByAgentId
      description: Soft-deletes an agent; existing in-progress calls are unaffected.
      x-rate-limit:
        name: agentMutationLimiter
      parameters:
        - $ref: '#/components/parameters/AgentId'
      responses:
        '204': { description: Deleted }
        '404': { $ref: '#/components/responses/NotFound' }

  /api/agents/{agentId}/signed-url:
    get:
      tags: [Agents]
      summary: Get signed ElevenLabs URL
      operationId: getAgentsByAgentIdSignedUrl
      description: Returns a short-lived signed URL for direct browser-to-ElevenLabs realtime conversation with this agent.
      parameters:
        - $ref: '#/components/parameters/AgentId'
      responses:
        '200':
          description: Signed URL
          content:
            application/json:
              schema:
                type: object
                properties:
                  signedUrl: { type: string, format: uri }
                  expiresAt: { type: string, format: date-time }

  /api/agents/{agentId}/ab-tests:
    get:
      tags: [Agents]
      summary: List A/B variants
      operationId: getAgentsByAgentIdAbTests
      description: Lists every prompt variant registered for the agent and their traffic weights.
      parameters:
        - $ref: '#/components/parameters/AgentId'
      responses:
        '200': { description: Variants list }
    post:
      tags: [Agents]
      summary: Create A/B variant
      operationId: postAgentsByAgentIdAbTests
      description: Creates a new prompt variant for the agent. The sum of variant weights should equal 100 to distribute traffic cleanly.
      x-rate-limit: { name: agentMutationLimiter }
      parameters:
        - $ref: '#/components/parameters/AgentId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name, systemPrompt]
              properties:
                name: { type: string, minLength: 1, maxLength: 100 }
                systemPrompt: { type: string, minLength: 10, maxLength: 5000 }
                firstMessage: { type: string, maxLength: 500 }
                weight: { type: number, minimum: 0, maximum: 100 }
      responses:
        '201': { description: Created }
        '400': { $ref: '#/components/responses/BadRequest' }

  /api/agents/{agentId}/ab-tests/results:
    get:
      tags: [Agents]
      summary: Compare A/B variant performance
      operationId: getAgentsByAgentIdAbTestsResults
      description: Returns call volume and outcome statistics for each variant.
      parameters:
        - $ref: '#/components/parameters/AgentId'
      responses:
        '200': { description: Results }

  /api/agents/{agentId}/ab-tests/{variantId}:
    patch:
      tags: [Agents]
      summary: Update A/B variant
      operationId: patchAgentsByAgentIdAbTestsByVariantId
      description: "Update A/B variant. Only the fields provided in the request body are changed."
      x-rate-limit: { name: agentMutationLimiter }
      parameters:
        - $ref: '#/components/parameters/AgentId'
        - name: variantId
          in: path
          required: true
          schema: { type: string, format: uuid }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                weight: { type: number, minimum: 0, maximum: 100 }
                isActive: { type: boolean }
                systemPrompt: { type: string, minLength: 10, maxLength: 5000 }
                firstMessage: { type: string, maxLength: 500 }
      responses:
        '200': { description: Updated }
        '404': { $ref: '#/components/responses/NotFound' }
    delete:
      tags: [Agents]
      summary: Delete A/B variant
      operationId: deleteAgentsByAgentIdAbTestsByVariantId
      description: "Delete A/B variant. Removes it from the current tenant."
      x-rate-limit: { name: agentMutationLimiter }
      parameters:
        - $ref: '#/components/parameters/AgentId'
        - name: variantId
          in: path
          required: true
          schema: { type: string, format: uuid }
      responses:
        '204': { description: Deleted }

  # ───────────────────────── Agent Tools ─────────────────────────
  /api/agents/{agentId}/tools:
    get:
      tags: [Agent Tools]
      summary: List agent tools
      operationId: getAgentsByAgentIdTools
      description: "List agent tools. Returns the current tenant's records, newest first, with pagination."
      parameters:
        - $ref: '#/components/parameters/AgentId'
      responses:
        '200': { description: Tools list }
    post:
      tags: [Agent Tools]
      summary: Create agent tool
      operationId: postAgentsByAgentIdTools
      description: Registers a new webhook / function tool the LLM may call during conversations.
      x-rate-limit: { name: agentMutationLimiter }
      parameters:
        - $ref: '#/components/parameters/AgentId'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/CreateToolInput' }
      responses:
        '201': { description: Created }
        '400': { $ref: '#/components/responses/BadRequest' }

  /api/agents/{agentId}/tools/{toolId}:
    get:
      tags: [Agent Tools]
      summary: Get tool by id
      operationId: getAgentsByAgentIdToolsByToolId
      description: "Get tool by id. Returns the record for the current tenant."
      parameters:
        - $ref: '#/components/parameters/AgentId'
        - name: toolId
          in: path
          required: true
          schema: { type: string, format: uuid }
      responses:
        '200': { description: Tool }
        '404': { $ref: '#/components/responses/NotFound' }
    patch:
      tags: [Agent Tools]
      summary: Update tool
      operationId: patchAgentsByAgentIdToolsByToolId
      description: "Update tool. Only the fields provided in the request body are changed."
      x-rate-limit: { name: agentMutationLimiter }
      parameters:
        - $ref: '#/components/parameters/AgentId'
        - name: toolId
          in: path
          required: true
          schema: { type: string, format: uuid }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              description: Partial tool update — all fields from the create schema are optional.
      responses:
        '200': { description: Updated }
    delete:
      tags: [Agent Tools]
      summary: Delete tool
      operationId: deleteAgentsByAgentIdToolsByToolId
      description: "Delete tool. Removes it from the current tenant."
      x-rate-limit: { name: agentMutationLimiter }
      parameters:
        - $ref: '#/components/parameters/AgentId'
        - name: toolId
          in: path
          required: true
          schema: { type: string, format: uuid }
      responses:
        '204': { description: Deleted }

  # ───────────────────────── Agent Workflow ─────────────────────────
  /api/agents/{agentId}/workflow:
    get:
      tags: [Agent Workflow]
      summary: Get agent workflow
      operationId: getAgentsByAgentIdWorkflow
      description: The agent's conversation workflow (subagent phases + branches), or null when the agent runs in plain single-prompt mode.
      parameters:
        - $ref: '#/components/parameters/AgentId'
      responses:
        '200': { description: Workflow graph or null }
        '404': { $ref: '#/components/responses/NotFound' }
    put:
      tags: [Agent Workflow]
      summary: Set agent workflow
      operationId: putAgentsByAgentIdWorkflow
      description: >-
        Replace the agent's conversation workflow. The graph is validated
        (unique slug ids, entryNodeId must exist, end nodes are terminal,
        llm branch conditions need text), persisted, and synced to the voice
        provider immediately — with rollback if the vendor sync fails.
      x-rate-limit: { name: agentMutationLimiter }
      parameters:
        - $ref: '#/components/parameters/AgentId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [version, entryNodeId, nodes, edges]
              properties:
                version: { type: integer, enum: [1] }
                templateId: { type: string, maxLength: 60, description: Informational — template this started from. }
                entryNodeId: { type: string, description: Node the conversation enters first. }
                nodes:
                  type: array
                  minItems: 1
                  maxItems: 12
                  items:
                    type: object
                    required: [id, type, label]
                    properties:
                      id: { type: string, description: 'Lowercase slug (^[a-z][a-z0-9_-]*$); "start"/"start_node" reserved.' }
                      type: { type: string, enum: [subagent, end] }
                      label: { type: string, maxLength: 80 }
                      prompt: { type: string, maxLength: 5000, description: Subagent only — phase instructions layered on the base prompt. }
                      llm: { type: string, maxLength: 100, description: Subagent only — per-phase LLM override. }
                      toolIds: { type: array, items: { type: string }, maxItems: 20 }
                      knowledgeBaseIds: { type: array, items: { type: string }, maxItems: 20 }
                edges:
                  type: array
                  maxItems: 24
                  items:
                    type: object
                    required: [id, source, target, condition]
                    properties:
                      id: { type: string }
                      source: { type: string }
                      target: { type: string }
                      condition:
                        type: object
                        required: [type]
                        properties:
                          type: { type: string, enum: [unconditional, llm] }
                          text: { type: string, maxLength: 500, description: Required when type is llm. }
      responses:
        '200': { description: Saved workflow }
        '400': { $ref: '#/components/responses/BadRequest' }
        '404': { $ref: '#/components/responses/NotFound' }
        '409': { description: Agent still provisioning — retry shortly }
    delete:
      tags: [Agent Workflow]
      summary: Remove agent workflow
      operationId: deleteAgentsByAgentIdWorkflow
      description: Clears the workflow — the agent returns to single-prompt mode. Idempotent.
      x-rate-limit: { name: agentMutationLimiter }
      parameters:
        - $ref: '#/components/parameters/AgentId'
      responses:
        '204': { description: Removed }

  # ───────────────────────── Knowledge ─────────────────────────
  /api/agents/{agentId}/knowledge:
    get:
      tags: [Knowledge]
      summary: List knowledge documents
      operationId: getAgentsByAgentIdKnowledge
      description: "List knowledge documents. Returns the current tenant's records, newest first, with pagination."
      parameters:
        - $ref: '#/components/parameters/AgentId'
      responses:
        '200': { description: Documents list }
    post:
      tags: [Knowledge]
      summary: Upload knowledge document
      operationId: postAgentsByAgentIdKnowledge
      description: Uploads a PDF, DOCX, TXT, CSV, HTML or EPUB file (max 20 MB) as an agent knowledge source.
      x-rate-limit: { name: agentMutationLimiter }
      parameters:
        - $ref: '#/components/parameters/AgentId'
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required: [file]
              properties:
                file:
                  type: string
                  format: binary
      responses:
        '201': { description: Uploaded }
        '402': { $ref: '#/components/responses/PaymentRequired' }
        '413': { description: File too large }

  /api/agents/{agentId}/knowledge/{docId}:
    delete:
      tags: [Knowledge]
      summary: Delete knowledge document
      operationId: deleteAgentsByAgentIdKnowledgeByDocId
      description: "Delete knowledge document. Removes it from the current tenant."
      parameters:
        - $ref: '#/components/parameters/AgentId'
        - name: docId
          in: path
          required: true
          schema: { type: string, format: uuid }
      responses:
        '204': { description: Deleted }
        '404': { $ref: '#/components/responses/NotFound' }

  # ───────────────────────── Agent MCP Servers ─────────────────────────
  /api/agents/{agentId}/mcp-servers:
    get:
      tags: [MCP Servers]
      summary: List MCP servers assigned to agent
      operationId: getAgentsByAgentIdMcpServers
      description: "List MCP servers assigned to agent. Returns the current tenant's records, newest first, with pagination."
      parameters:
        - $ref: '#/components/parameters/AgentId'
      responses:
        '200': { description: Assignments }
    post:
      tags: [MCP Servers]
      summary: Assign MCP server to agent
      operationId: postAgentsByAgentIdMcpServers
      description: "Assign MCP server to agent. Creates the resource for the current tenant and returns it."
      parameters:
        - $ref: '#/components/parameters/AgentId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [mcpServerId]
              properties:
                mcpServerId: { type: string, format: uuid }
      responses:
        '201': { description: Assigned }

  /api/agents/{agentId}/mcp-servers/{mcpServerId}:
    delete:
      tags: [MCP Servers]
      summary: Remove MCP server from agent
      operationId: deleteAgentsByAgentIdMcpServersByMcpServerId
      description: "Remove MCP server from agent. Removes it from the current tenant."
      parameters:
        - $ref: '#/components/parameters/AgentId'
        - name: mcpServerId
          in: path
          required: true
          schema: { type: string, format: uuid }
      responses:
        '204': { description: Removed }

  # ───────────────────────── Campaigns ─────────────────────────
  /api/campaigns:
    get:
      tags: [Campaigns]
      summary: List campaigns
      operationId: getCampaigns
      description: "List campaigns. Returns the current tenant's records, newest first, with pagination."
      parameters:
        - $ref: '#/components/parameters/Page'
        - $ref: '#/components/parameters/Limit'
      responses:
        '200':
          description: Campaigns list
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: '#/components/schemas/Campaign' }
    post:
      tags: [Campaigns]
      summary: Create campaign
      operationId: postCampaigns
      description: Creates an outbound campaign bound to an agent, schedule and channel. Gated by the `campaigns` billing feature.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/CreateCampaignInput' }
      responses:
        '201':
          description: Created
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Campaign' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '402': { $ref: '#/components/responses/PaymentRequired' }

  /api/campaigns/{campaignId}:
    get:
      tags: [Campaigns]
      summary: Get campaign by id
      operationId: getCampaignsByCampaignId
      description: "Get campaign by id. Returns the record for the current tenant."
      parameters:
        - $ref: '#/components/parameters/CampaignId'
      responses:
        '200':
          description: Campaign
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Campaign' }
        '404': { $ref: '#/components/responses/NotFound' }
    patch:
      tags: [Campaigns]
      summary: Update campaign
      operationId: patchCampaignsByCampaignId
      description: "Update campaign. Only the fields provided in the request body are changed."
      x-rate-limit: { name: agentMutationLimiter }
      parameters:
        - $ref: '#/components/parameters/CampaignId'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/UpdateCampaignInput' }
      responses:
        '200':
          description: Updated
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Campaign' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '404': { $ref: '#/components/responses/NotFound' }
    delete:
      tags: [Campaigns]
      summary: Delete campaign
      operationId: deleteCampaignsByCampaignId
      description: "Delete campaign. Removes it from the current tenant."
      x-rate-limit: { name: agentMutationLimiter }
      parameters:
        - $ref: '#/components/parameters/CampaignId'
      responses:
        '204': { description: Deleted }
        '404': { $ref: '#/components/responses/NotFound' }

  /api/campaigns/{campaignId}/activate:
    patch:
      tags: [Campaigns]
      summary: Activate campaign
      operationId: patchCampaignsByCampaignIdActivate
      description: Transitions the campaign from `draft` or `paused` to `active`, enabling the dialer to pick up contacts.
      x-rate-limit: { name: agentMutationLimiter }
      parameters:
        - $ref: '#/components/parameters/CampaignId'
      responses:
        '200': { description: Activated }
        '409': { $ref: '#/components/responses/Conflict' }

  /api/campaigns/{campaignId}/pause:
    patch:
      tags: [Campaigns]
      summary: Pause campaign
      operationId: patchCampaignsByCampaignIdPause
      description: Halts new call dispatch for the campaign; in-flight calls complete normally.
      x-rate-limit: { name: agentMutationLimiter }
      parameters:
        - $ref: '#/components/parameters/CampaignId'
      responses:
        '200': { description: Paused }

  # ───────────────────────── Contacts ─────────────────────────
  /api/campaigns/{campaignId}/contacts:
    get:
      tags: [Contacts]
      summary: List contacts in campaign
      operationId: getCampaignsByCampaignIdContacts
      description: "List contacts in campaign. Returns the current tenant's records, newest first, with pagination."
      parameters:
        - $ref: '#/components/parameters/CampaignId'
        - $ref: '#/components/parameters/Page'
        - $ref: '#/components/parameters/Limit'
        - name: status
          in: query
          schema: { type: string }
      responses:
        '200':
          description: Contacts list
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: '#/components/schemas/Contact' }
                  total: { type: integer }
    post:
      tags: [Contacts]
      summary: Add a single contact
      operationId: postCampaignsByCampaignIdContacts
      description: "Add a single contact. Creates the resource for the current tenant and returns it."
      parameters:
        - $ref: '#/components/parameters/CampaignId'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/CreateContactInput' }
      responses:
        '201':
          description: Created
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Contact' }
        '400': { $ref: '#/components/responses/BadRequest' }

  /api/campaigns/{campaignId}/contacts/bulk:
    post:
      tags: [Contacts]
      summary: Bulk add contacts
      operationId: postCampaignsByCampaignIdContactsBulk
      description: Adds up to 5000 contacts in a single call. Duplicates within the campaign are deduplicated server-side.
      x-rate-limit: { name: bulkImportLimiter }
      parameters:
        - $ref: '#/components/parameters/CampaignId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [contacts]
              properties:
                contacts:
                  type: array
                  minItems: 1
                  maxItems: 5000
                  items: { $ref: '#/components/schemas/CreateContactInput' }
      responses:
        '201': { description: Bulk import accepted }
        '400': { $ref: '#/components/responses/BadRequest' }

  /api/campaigns/{campaignId}/contacts/upload-preview:
    post:
      tags: [Contacts]
      summary: Preview CSV/Excel upload
      operationId: postCampaignsByCampaignIdContactsUploadPreview
      description: Parses the uploaded file and returns a column-mapping preview without persisting rows.
      parameters:
        - $ref: '#/components/parameters/CampaignId'
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required: [file]
              properties:
                file:
                  type: string
                  format: binary
                  description: CSV or Excel file (max 10 MB)
      responses:
        '200': { description: Parsed preview }
        '400': { $ref: '#/components/responses/BadRequest' }

  /api/campaigns/{campaignId}/contacts/upload-confirm:
    post:
      tags: [Contacts]
      summary: Confirm CSV/Excel upload
      operationId: postCampaignsByCampaignIdContactsUploadConfirm
      description: Persists the rows from the uploaded CSV/Excel file using the confirmed column mapping.
      x-rate-limit: { name: bulkImportLimiter }
      parameters:
        - $ref: '#/components/parameters/CampaignId'
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required: [file]
              properties:
                file: { type: string, format: binary }
                mapping:
                  type: string
                  description: JSON-encoded column-to-field mapping.
      responses:
        '201': { description: Upload persisted }

  /api/campaigns/{campaignId}/contacts/upload-template:
    get:
      tags: [Contacts]
      summary: Download CSV template
      operationId: getCampaignsByCampaignIdContactsUploadTemplate
      description: "Download CSV template. Returns the record for the current tenant."
      parameters:
        - $ref: '#/components/parameters/CampaignId'
      responses:
        '200':
          description: CSV template
          content:
            text/csv: {}

  # ───────────────────────── Analytics ─────────────────────────
  /api/analytics/overview:
    get:
      tags: [Analytics]
      summary: Tenant overview metrics
      operationId: getAnalyticsOverview
      description: Returns top-level tenant KPIs — total calls, success rate, average duration, minutes used.
      responses:
        '200': { description: Overview }
        '429': { $ref: '#/components/responses/RateLimited' }
  /api/analytics/overview/by-direction:
    get:
      tags: [Analytics]
      summary: Overview split by inbound vs outbound
      operationId: getAnalyticsOverviewByDirection
      description: "Overview split by inbound vs outbound. Returns the record for the current tenant."
      responses:
        '200': { description: Split overview }
  /api/analytics/calls-per-day:
    get:
      tags: [Analytics]
      summary: Call volume per day
      operationId: getAnalyticsCallsPerDay
      description: "Call volume per day. Returns the record for the current tenant."
      parameters:
        - name: from
          in: query
          schema: { type: string, format: date }
        - name: to
          in: query
          schema: { type: string, format: date }
      responses:
        '200': { description: Daily series }
  /api/analytics/calls-by-hour:
    get:
      tags: [Analytics]
      summary: Call volume by hour of day
      operationId: getAnalyticsCallsByHour
      description: "Call volume by hour of day. Returns the record for the current tenant."
      responses:
        '200': { description: Hourly distribution }
  /api/analytics/sentiment-trend:
    get:
      tags: [Analytics]
      summary: Sentiment trend
      operationId: getAnalyticsSentimentTrend
      description: "Sentiment trend. Returns the record for the current tenant."
      responses:
        '200': { description: Sentiment series }
  /api/analytics/top-campaigns:
    get:
      tags: [Analytics]
      summary: Top campaigns by volume
      operationId: getAnalyticsTopCampaigns
      description: "Top campaigns by volume. Returns the record for the current tenant."
      responses:
        '200': { description: Ranked campaigns }
  /api/analytics/campaigns/{campaignId}:
    get:
      tags: [Analytics]
      summary: Campaign analytics
      operationId: getAnalyticsCampaignsByCampaignId
      description: "Campaign analytics. Returns the record for the current tenant."
      parameters:
        - $ref: '#/components/parameters/CampaignId'
      responses:
        '200': { description: Campaign analytics }

  /api/analytics/events:
    get:
      tags: [Analytics]
      summary: List tracked events
      operationId: getAnalyticsEvents
      description: Query the tenant event stream by type, time range or correlation id.
      parameters:
        - name: type
          in: query
          schema: { type: string }
        - name: from
          in: query
          schema: { type: string, format: date-time }
        - name: to
          in: query
          schema: { type: string, format: date-time }
        - $ref: '#/components/parameters/Limit'
      responses:
        '200': { description: Events }
    post:
      tags: [Analytics]
      summary: Ingest a single event
      operationId: postAnalyticsEvents
      description: "Ingest a single event."
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [eventType]
              properties:
                eventType: { type: string }
                properties: { type: object }
                occurredAt: { type: string, format: date-time }
      responses:
        '202': { description: Accepted }
        '400': { $ref: '#/components/responses/BadRequest' }

  /api/analytics/events/batch:
    post:
      tags: [Analytics]
      summary: Ingest a batch of events
      operationId: postAnalyticsEventsBatch
      description: Up to a few hundred events per call. A stricter limiter of 30 requests/min/tenant applies.
      x-rate-limit: { name: batchIngestLimiter, windowMs: 60000, max: 30 }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [events]
              properties:
                events:
                  type: array
                  items:
                    type: object
                    required: [eventType]
                    properties:
                      eventType: { type: string }
                      properties: { type: object }
                      occurredAt: { type: string, format: date-time }
      responses:
        '202': { description: Accepted }
        '429': { $ref: '#/components/responses/RateLimited' }

  /api/analytics/events/summary:
    get:
      tags: [Analytics]
      summary: Event summary grouped by type
      operationId: getAnalyticsEventsSummary
      description: "Event summary grouped by type. Returns the record for the current tenant."
      responses:
        '200': { description: Summary }

  /api/analytics/kpi/tenant-summary:
    get:
      tags: [Analytics]
      summary: Tenant KPI summary
      operationId: getAnalyticsKpiTenantSummary
      description: "Tenant KPI summary. Returns the record for the current tenant."
      parameters:
        - name: days
          in: query
          description: Look-back window in days (1–365). Defaults to 30.
          schema: { type: integer, minimum: 1, maximum: 365, default: 30 }
      responses:
        '200': { description: KPI summary }

  /api/analytics/kpi/templates/{category}:
    get:
      tags: [Analytics]
      summary: Get KPI template by category
      operationId: getAnalyticsKpiTemplatesByCategory
      description: "Get KPI template by category. Returns the record for the current tenant."
      parameters:
        - name: category
          in: path
          required: true
          schema: { type: string }
      responses:
        '200': { description: Template }

  /api/analytics/kpi/{campaignId}:
    get:
      tags: [Analytics]
      summary: Get campaign KPIs
      operationId: getAnalyticsKpiByCampaignId
      description: "Get campaign KPIs. Returns the record for the current tenant."
      parameters:
        - $ref: '#/components/parameters/CampaignId'
        - name: days
          in: query
          description: Look-back window in days (1–365). Defaults to 30.
          schema: { type: integer, minimum: 1, maximum: 365, default: 30 }
      responses:
        '200': { description: KPIs }

  /api/analytics/kpi/{campaignId}/timeseries:
    get:
      tags: [Analytics]
      summary: Campaign KPI time series
      operationId: getAnalyticsKpiByCampaignIdTimeseries
      description: |
        Time series for a single KPI of a campaign. The `kpi` query parameter is
        **required** — pass one of the KPI keys returned by
        `GET /api/analytics/kpi/templates/{category}` (e.g. `revenue_total`).
        Omitting it returns `400 VALIDATION_FAILED`.
      parameters:
        - $ref: '#/components/parameters/CampaignId'
        - name: kpi
          in: query
          required: true
          description: KPI key to chart (e.g. `revenue_total`). See the KPI template for valid keys.
          schema: { type: string, minLength: 1, maxLength: 100 }
        - name: days
          in: query
          description: Look-back window in days (1–365). Defaults to 30.
          schema: { type: integer, minimum: 1, maximum: 365, default: 30 }
      responses:
        '200': { description: Time series }
        '400': { $ref: '#/components/responses/BadRequest' }

  /api/analytics/kpi/{campaignId}/objections:
    get:
      tags: [Analytics]
      summary: Objection ranking for campaign
      operationId: getAnalyticsKpiByCampaignIdObjections
      description: "Objection ranking for campaign. Returns the record for the current tenant."
      parameters:
        - $ref: '#/components/parameters/CampaignId'
        - name: days
          in: query
          description: Look-back window in days (1–365). Defaults to 30.
          schema: { type: integer, minimum: 1, maximum: 365, default: 30 }
        - name: limit
          in: query
          description: Max objections to return (1–50). Defaults to 10.
          schema: { type: integer, minimum: 1, maximum: 50, default: 10 }
      responses:
        '200': { description: Objection ranking }

  # ───────────────────────── Dashboard ─────────────────────────
  /api/dashboard:
    get:
      tags: [Dashboard]
      summary: Dashboard bundle
      operationId: getDashboard
      description: Returns pre-aggregated tiles suitable for a home dashboard — recent calls, KPIs and health.
      responses:
        '200': { description: Dashboard bundle }

  # ───────────────────────── Conversations ─────────────────────────
  /api/conversations:
    get:
      tags: [Conversations]
      summary: List conversations
      operationId: getConversations
      description: "List conversations. Returns the current tenant's records, newest first, with pagination."
      parameters:
        - $ref: '#/components/parameters/Page'
        - $ref: '#/components/parameters/Limit'
        - name: channel
          in: query
          schema:
            type: string
            enum: [voice, whatsapp, webchat, email]
        - name: status
          in: query
          schema: { type: string }
      responses:
        '200':
          description: Conversations list
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: '#/components/schemas/Conversation' }
  /api/conversations/{conversationId}:
    get:
      tags: [Conversations]
      summary: Get conversation with messages
      operationId: getConversationsByConversationId
      description: "Get conversation with messages. Returns the current tenant's records, newest first, with pagination."
      parameters:
        - name: conversationId
          in: path
          required: true
          schema: { type: string, format: uuid }
      responses:
        '200':
          description: Conversation
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Conversation'
                  - type: object
                    properties:
                      messages:
                        type: array
                        items: { $ref: '#/components/schemas/Message' }
        '404': { $ref: '#/components/responses/NotFound' }

  # ───────────────────────── Webchat ─────────────────────────
  /api/webchat/message:
    post:
      tags: [Webchat]
      summary: Send webchat message
      operationId: postWebchatMessage
      description: Posts a message into an embedded web-chat conversation. Returns the AI response inline.
      x-rate-limit: { name: webchatLimiter }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [conversationId, message]
              properties:
                conversationId: { type: string, format: uuid }
                message: { type: string, minLength: 1, maxLength: 4000 }
      responses:
        '200': { description: AI response }

  /api/webchat/{conversationId}/history:
    get:
      tags: [Webchat]
      summary: Get webchat history
      operationId: getWebchatByConversationIdHistory
      description: "Get webchat history. Returns the record for the current tenant."
      parameters:
        - name: conversationId
          in: path
          required: true
          schema: { type: string, format: uuid }
      responses:
        '200': { description: Messages }

  /api/webchat/{conversationId}/close:
    post:
      tags: [Webchat]
      summary: Close webchat conversation
      operationId: postWebchatByConversationIdClose
      description: "Close webchat conversation."
      parameters:
        - name: conversationId
          in: path
          required: true
          schema: { type: string, format: uuid }
      responses:
        '200': { description: Closed }

  # ───────────────────────── WhatsApp / Email ─────────────────────────
  /api/whatsapp/send:
    post:
      tags: [WhatsApp]
      summary: Send WhatsApp message
      operationId: postWhatsappSend
      description: "Send WhatsApp message."
      x-rate-limit: { name: outboundLimiter }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [to, agentId]
              properties:
                to: { type: string, description: E.164 WhatsApp phone number }
                agentId: { type: string, format: uuid }
                contactId: { type: string, format: uuid }
                message: { type: string }
                templateName: { type: string }
                templateVariables: { type: object }
      responses:
        '202': { description: Queued }

  /api/email/send:
    post:
      tags: [Email]
      summary: Send email message
      operationId: postEmailSend
      description: "Send email message."
      x-rate-limit: { name: outboundLimiter }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [to, agentId, subject]
              properties:
                to: { type: string, format: email }
                agentId: { type: string, format: uuid }
                subject: { type: string, maxLength: 200 }
                body: { type: string }
                contactId: { type: string, format: uuid }
      responses:
        '202': { description: Queued }

  # ───────────────────────── Integrations ─────────────────────────
  /api/integrations:
    get:
      tags: [Integrations]
      summary: List integrations
      operationId: getIntegrations
      description: "List integrations. Returns the current tenant's records, newest first, with pagination."
      responses:
        '200': { description: Integrations list }
    post:
      tags: [Integrations]
      summary: Create integration
      operationId: postIntegrations
      description: Registers a CRM integration with either credentials, an API key, or an OAuth2 placeholder to be completed via the connect flow.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [provider, credentials]
              properties:
                provider:
                  type: string
                  enum: [hubspot, salesforce, zoho, pipedrive, freshsales, airtable, google_sheets, webhook, dynamics365, zendesk, servicenow, kommo, monday, bitrix24, intercom, notion]
                label: { type: string, maxLength: 200 }
                credentials: { type: object, additionalProperties: true }
                fieldMapping:
                  type: object
                  additionalProperties: { type: string }
                campaignId: { type: string, format: uuid }
      responses:
        '201': { description: Created }
        '400': { $ref: '#/components/responses/BadRequest' }
        '402': { $ref: '#/components/responses/PaymentRequired' }

  /api/integrations/{integrationId}:
    get:
      tags: [Integrations]
      summary: Get integration by id
      operationId: getIntegrationsByIntegrationId
      description: "Get integration by id. Returns the record for the current tenant."
      parameters:
        - $ref: '#/components/parameters/IntegrationId'
      responses:
        '200': { description: Integration }
        '404': { $ref: '#/components/responses/NotFound' }
    patch:
      tags: [Integrations]
      summary: Update integration
      operationId: patchIntegrationsByIntegrationId
      description: "Update integration. Only the fields provided in the request body are changed."
      parameters:
        - $ref: '#/components/parameters/IntegrationId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                label: { type: string, maxLength: 200 }
                credentials: { type: object }
                fieldMapping:
                  type: object
                  additionalProperties: { type: string }
                campaignId:
                  type: [string, "null"]
                  format: uuid
                status:
                  type: string
                  enum: [active, paused]
      responses:
        '200': { description: Updated }
    delete:
      tags: [Integrations]
      summary: Delete integration
      operationId: deleteIntegrationsByIntegrationId
      description: "Delete integration. Removes it from the current tenant."
      parameters:
        - $ref: '#/components/parameters/IntegrationId'
      responses:
        '204': { description: Deleted }

  /api/integrations/{integrationId}/test:
    post:
      tags: [Integrations]
      summary: Test integration connection
      operationId: postIntegrationsByIntegrationIdTest
      description: "Test integration connection."
      parameters:
        - $ref: '#/components/parameters/IntegrationId'
      responses:
        '200': { description: Connection ok }
        '400': { $ref: '#/components/responses/BadRequest' }

  /api/integrations/{integrationId}/discover:
    post:
      tags: [Integrations]
      summary: Discover remote schema
      operationId: postIntegrationsByIntegrationIdDiscover
      description: Introspects the remote CRM to list its fields for mapping.
      parameters:
        - $ref: '#/components/parameters/IntegrationId'
      responses:
        '200': { description: Schema }

  /api/integrations/{integrationId}/ai-map:
    post:
      tags: [Integrations]
      summary: AI-assisted field mapping
      operationId: postIntegrationsByIntegrationIdAiMap
      description: Uses an LLM to propose a CRM-to-Jelliu field mapping from the discovered schema.
      parameters:
        - $ref: '#/components/parameters/IntegrationId'
      responses:
        '200': { description: Suggested mapping }

  /api/integrations/{integrationId}/preview:
    post:
      tags: [Integrations]
      summary: Preview sync output
      operationId: postIntegrationsByIntegrationIdPreview
      description: Returns a dry-run of the first N transformed contacts from the CRM without persisting.
      parameters:
        - $ref: '#/components/parameters/IntegrationId'
      responses:
        '200': { description: Preview rows }

  /api/integrations/{integrationId}/sync:
    post:
      tags: [Integrations]
      summary: Trigger sync
      operationId: postIntegrationsByIntegrationIdSync
      description: "Trigger sync."
      parameters:
        - $ref: '#/components/parameters/IntegrationId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [campaignId]
              properties:
                campaignId: { type: string, format: uuid }
                fullSync: { type: boolean, default: false }
      responses:
        '202': { description: Sync enqueued }

  /api/integrations/{integrationId}/webhook-schema:
    get:
      tags: [Integrations]
      summary: Get inbound webhook schema
      operationId: getIntegrationsByIntegrationIdWebhookSchema
      description: "Get inbound webhook schema. Returns the record for the current tenant."
      parameters:
        - $ref: '#/components/parameters/IntegrationId'
      responses:
        '200': { description: Schema }

  /api/integrations/oauth/authorize:
    get:
      tags: [Integrations]
      summary: Start OAuth connect flow
      operationId: getIntegrationsOauthAuthorize
      description: Returns an `authorizeUrl` to redirect the browser to — the provider will later invoke `/webhooks/integrations/oauth/callback`.
      parameters:
        - name: provider
          in: query
          required: true
          schema: { type: string }
        - name: redirect_url
          in: query
          schema: { type: string, format: uri }
      responses:
        '200':
          description: Authorize URL
          content:
            application/json:
              schema:
                type: object
                properties:
                  authorizeUrl: { type: string, format: uri }

  /api/integrations/oauth/refresh/{integrationId}:
    post:
      tags: [Integrations]
      summary: Manually refresh OAuth token
      operationId: postIntegrationsOauthRefreshByIntegrationId
      description: "Manually refresh OAuth token."
      parameters:
        - $ref: '#/components/parameters/IntegrationId'
      responses:
        '200': { description: Refreshed }

  /api/integrations/oauth/status/{integrationId}:
    get:
      tags: [Integrations]
      summary: OAuth token status
      operationId: getIntegrationsOauthStatusByIntegrationId
      description: "OAuth token status. Returns the current tenant's records, newest first, with pagination."
      parameters:
        - $ref: '#/components/parameters/IntegrationId'
      responses:
        '200':
          description: Status
          content:
            application/json:
              schema:
                type: object
                properties:
                  integrationId: { type: string, format: uuid }
                  provider: { type: string }
                  authMethod: { type: string, enum: [credentials, oauth2, api_key] }
                  tokenStatus:
                    type: string
                    enum: [valid, expired, expiring_soon, missing, no_expiry]
                  expiresAt:
                    type: [string, "null"]
                    format: date-time
                  scopes:
                    type: [array, "null"]
                    items: { type: string }
                  integrationStatus: { type: string }

  # ───────────────────────── Outbound Webhooks ─────────────────────────
  /api/webhooks:
    get:
      tags: [Webhooks]
      summary: List outbound webhooks
      operationId: getWebhooks
      description: "List outbound webhooks. Returns the current tenant's records, newest first, with pagination."
      responses:
        '200':
          description: Webhooks
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: '#/components/schemas/Webhook' }
    post:
      tags: [Webhooks]
      summary: Create outbound webhook
      operationId: postWebhooks
      description: "Create outbound webhook. Creates the resource for the current tenant and returns it."
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/CreateWebhookInput' }
      responses:
        '201':
          description: Created
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Webhook' }
        '400': { $ref: '#/components/responses/BadRequest' }

  /api/webhooks/{webhookId}:
    get:
      tags: [Webhooks]
      summary: Get webhook by id
      operationId: getWebhooksByWebhookId
      description: "Get webhook by id. Returns the record for the current tenant."
      parameters:
        - $ref: '#/components/parameters/WebhookId'
      responses:
        '200':
          description: Webhook
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Webhook' }
        '404': { $ref: '#/components/responses/NotFound' }
    patch:
      tags: [Webhooks]
      summary: Update webhook
      operationId: patchWebhooksByWebhookId
      description: "Update webhook. Only the fields provided in the request body are changed."
      parameters:
        - $ref: '#/components/parameters/WebhookId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              description: Partial update.
      responses:
        '200': { description: Updated }
    delete:
      tags: [Webhooks]
      summary: Delete webhook
      operationId: deleteWebhooksByWebhookId
      description: "Delete webhook. Removes it from the current tenant."
      parameters:
        - $ref: '#/components/parameters/WebhookId'
      responses:
        '204': { description: Deleted }

  /api/webhooks/{webhookId}/rotate-secret:
    post:
      tags: [Webhooks]
      summary: Rotate webhook signing secret
      operationId: postWebhooksByWebhookIdRotateSecret
      description: "Rotate webhook signing secret."
      parameters:
        - $ref: '#/components/parameters/WebhookId'
      responses:
        '200':
          description: New secret
          content:
            application/json:
              schema:
                type: object
                properties:
                  secret: { type: string }

  /api/webhooks/{webhookId}/delivery-logs:
    get:
      tags: [Webhooks]
      summary: Delivery logs
      operationId: getWebhooksByWebhookIdDeliveryLogs
      description: "Delivery logs. Returns the record for the current tenant."
      parameters:
        - $ref: '#/components/parameters/WebhookId'
        - $ref: '#/components/parameters/Limit'
      responses:
        '200': { description: Delivery attempts }

  # ───────────────────────── Widgets ─────────────────────────
  /api/widgets:
    get:
      tags: [Widgets]
      summary: List widgets
      operationId: getWidgets
      description: "List widgets. Returns the current tenant's records, newest first, with pagination."
      responses:
        '200': { description: Widgets }
    post:
      tags: [Widgets]
      summary: Create widget
      operationId: postWidgets
      description: "Create widget. Creates the resource for the current tenant and returns it."
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/CreateWidgetInput' }
      responses:
        '201':
          description: Created
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Widget' }

  /api/widgets/{id}:
    get:
      tags: [Widgets]
      summary: Get widget by id
      operationId: getWidgetsById
      description: "Get widget by id. Returns the record for the current tenant."
      parameters:
        - $ref: '#/components/parameters/WidgetId'
      responses:
        '200':
          description: Widget
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Widget' }
    put:
      tags: [Widgets]
      summary: Update widget
      operationId: putWidgetsById
      description: "Update widget."
      parameters:
        - $ref: '#/components/parameters/WidgetId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              description: Partial widget update.
      responses:
        '200': { description: Updated }
    delete:
      tags: [Widgets]
      summary: Delete widget
      operationId: deleteWidgetsById
      description: "Delete widget. Removes it from the current tenant."
      parameters:
        - $ref: '#/components/parameters/WidgetId'
      responses:
        '204': { description: Deleted }

  /api/widgets/{id}/embed.js:
    get:
      tags: [Widgets]
      summary: Download embed script (authenticated)
      operationId: getWidgetsByIdEmbedJs
      description: "Download embed script (authenticated). Returns the record for the current tenant."
      parameters:
        - $ref: '#/components/parameters/WidgetId'
      responses:
        '200':
          description: JavaScript embed bundle
          content:
            application/javascript: {}

  # ───────────────────────── Zapier ─────────────────────────
  /api/zapier:
    get:
      tags: [Zapier]
      summary: List Zapier subscriptions
      operationId: getZapier
      description: "List Zapier subscriptions. Returns the current tenant's records, newest first, with pagination."
      responses:
        '200': { description: Subscriptions }

  /api/zapier/api-key:
    post:
      tags: [Zapier]
      summary: Generate Zapier API key
      operationId: postZapierApiKey
      description: Mints a new `zk_`-prefixed key used by Zapier to authenticate against the public `/zapier/*` triggers. Admin only.
      responses:
        '201':
          description: Plaintext key (shown once)
          content:
            application/json:
              schema:
                type: object
                properties:
                  id: { type: string, format: uuid }
                  plaintext: { type: string, example: "zk_a1b2c3..." }

  /api/zapier/api-key/{id}:
    delete:
      tags: [Zapier]
      summary: Revoke Zapier API key
      operationId: deleteZapierApiKeyById
      description: "Revoke Zapier API key. Removes it from the current tenant."
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string, format: uuid }
      responses:
        '204': { description: Revoked }

  /zapier/subscribe:
    post:
      tags: [Zapier]
      summary: Create Zapier subscription (REST Hook)
      operationId: postZapierSubscribe
      description: Called BY Zapier using an `x-zapier-api-key` header (not Clerk or `jl_`). Creates a webhook subscription for a trigger event.
      security:
        - ZapierKey: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [event, target_url]
              properties:
                event:
                  type: string
                  enum: [call.completed, call.failed, campaign.completed, contact.converted, contact.dnc, contact.created, call.voicemail, call.escalated]
                target_url: { type: string, format: uri }
      responses:
        '201':
          description: Subscription created
          content:
            application/json:
              schema:
                type: object
                properties:
                  id: { type: string, format: uuid }

  /zapier/subscribe/{id}:
    delete:
      tags: [Zapier]
      summary: Remove Zapier subscription
      operationId: deleteZapierSubscribeById
      description: "Remove Zapier subscription. Removes it from the current tenant."
      security:
        - ZapierKey: []
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string, format: uuid }
      responses:
        '204': { description: Removed }

  /zapier/triggers/{event}:
    get:
      tags: [Zapier]
      summary: Sample events for Zapier trigger setup
      operationId: getZapierTriggersByEvent
      description: Returns recent events matching the trigger so Zapier can populate sample data in the editor.
      security:
        - ZapierKey: []
      parameters:
        - name: event
          in: path
          required: true
          schema: { type: string }
        - name: page
          in: query
          schema: { type: integer, minimum: 1 }
        - $ref: '#/components/parameters/Limit'
      responses:
        '200': { description: Sample events }

  # ───────────────────────── API Keys ─────────────────────────
  /api/api-keys:
    get:
      tags: [API Keys]
      summary: List API keys
      operationId: getApiKeys
      description: "List API keys. Returns the current tenant's records, newest first, with pagination."
      responses:
        '200':
          description: Keys
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: '#/components/schemas/ApiKey' }
    post:
      tags: [API Keys]
      summary: Create API key
      operationId: postApiKeys
      description: Mints a new `jl_`-prefixed bearer key. The plaintext value is only returned once. Admin only, and API-key-auth callers are forbidden from minting new keys.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name]
              properties:
                name: { type: string, minLength: 1, maxLength: 200 }
                scopes:
                  type: array
                  items: { type: string, maxLength: 64 }
                  maxItems: 20
                expires_at: { type: string, format: date-time }
      responses:
        '201':
          description: Created
          content:
            application/json:
              schema: { $ref: '#/components/schemas/CreatedApiKey' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '403': { $ref: '#/components/responses/Forbidden' }

  /api/api-keys/{id}:
    delete:
      tags: [API Keys]
      summary: Revoke API key
      operationId: deleteApiKeysById
      description: Revokes an existing API key. Forbidden when called with API-key auth (prevents a leaked key from locking out its own tenant).
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string, format: uuid }
      responses:
        '204': { description: Revoked }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }

  # ───────────────────────── Exports ─────────────────────────
  /api/exports:
    get:
      tags: [Exports]
      summary: List export jobs
      operationId: getExports
      description: "List export jobs. Returns the current tenant's records, newest first, with pagination."
      responses:
        '200': { description: Jobs }
    post:
      tags: [Exports]
      summary: Create export job
      operationId: postExports
      description: Queues an async export job. Poll `/api/exports/{id}` for status or `/api/exports/{id}/download` once completed.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [type]
              properties:
                type:
                  type: string
                  enum: [calls, contacts, conversations, analytics]
                format:
                  type: string
                  enum: [csv, json]
                  default: csv
                filters:
                  type: object
                  properties:
                    date_from: { type: string, format: date-time }
                    date_to: { type: string, format: date-time }
                    campaign_id: { type: string, format: uuid }
                    agent_id: { type: string, format: uuid }
                    status: { type: string }
                    outcome: { type: string }
      responses:
        '201':
          description: Export job queued
          content:
            application/json:
              schema:
                type: object
                properties:
                  id: { type: string, format: uuid }
                  status:
                    type: string
                    enum: [pending, processing, completed, failed]
        '402': { $ref: '#/components/responses/PaymentRequired' }

  /api/exports/{id}:
    get:
      tags: [Exports]
      summary: Get export job
      operationId: getExportsById
      description: "Get export job. Returns the record for the current tenant."
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string, format: uuid }
      responses:
        '200': { description: Job }
        '404': { $ref: '#/components/responses/NotFound' }
    delete:
      tags: [Exports]
      summary: Delete export job
      operationId: deleteExportsById
      description: "Delete export job. Removes it from the current tenant."
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string, format: uuid }
      responses:
        '204': { description: Deleted }

  /api/exports/{id}/download:
    get:
      tags: [Exports]
      summary: Download export file
      operationId: getExportsByIdDownload
      description: "Download export file. Returns the record for the current tenant."
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string, format: uuid }
      responses:
        '200':
          description: Export payload (CSV or JSON)
          content:
            text/csv: {}
            application/json: {}
        '404': { $ref: '#/components/responses/NotFound' }

  # ───────────────────────── Onboarding ─────────────────────────
  /api/onboarding/complete-profile:
    post:
      tags: [Onboarding]
      summary: Save onboarding profile
      operationId: postOnboardingCompleteProfile
      description: "Save onboarding profile."
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [companyName, industry, teamSize, goal, plan]
              properties:
                companyName: { type: string, minLength: 1, maxLength: 200 }
                industry: { type: string }
                teamSize: { type: string }
                goal: { type: string }
                goalOther: { type: string }
                plan:
                  type: string
                  enum: [starter, growth, business, enterprise]
      responses:
        '200': { description: Saved }

  /api/onboarding/templates:
    get:
      tags: [Onboarding]
      summary: List agent templates
      operationId: getOnboardingTemplates
      description: "List agent templates. Returns the current tenant's records, newest first, with pagination."
      parameters:
        - name: industry
          in: query
          schema:
            type: string
            enum: [telco, finance, retail, healthcare, insurance, real_estate, general]
        - name: category
          in: query
          schema: { type: string }
        - name: language
          in: query
          schema: { type: string }
      responses:
        '200': { description: Templates }
    post:
      tags: [Onboarding]
      summary: Create custom agent template
      operationId: postOnboardingTemplates
      description: "Create custom agent template. Creates the resource for the current tenant and returns it."
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name, industry, category, system_prompt, first_message, voice_id]
              properties:
                name: { type: string, maxLength: 100 }
                industry:
                  type: string
                  enum: [telco, finance, retail, healthcare, insurance, real_estate, general]
                category: { type: string }
                language: { type: string, default: es }
                description: { type: string, maxLength: 1000 }
                system_prompt: { type: string, minLength: 10, maxLength: 5000 }
                first_message: { type: string, minLength: 1, maxLength: 500 }
                voice_id: { type: string }
                max_call_duration_seconds: { type: integer, minimum: 30, maximum: 3600, default: 600 }
      responses:
        '201': { description: Created }

  /api/onboarding/templates/{id}:
    get:
      tags: [Onboarding]
      summary: Get template by id
      operationId: getOnboardingTemplatesById
      description: "Get template by id. Returns the record for the current tenant."
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string, format: uuid }
      responses:
        '200': { description: Template }
        '404': { $ref: '#/components/responses/NotFound' }

  /api/onboarding/templates/{id}/create-agent:
    post:
      tags: [Onboarding]
      summary: Instantiate agent from template
      operationId: postOnboardingTemplatesByIdCreateAgent
      description: "Instantiate agent from template. Creates the resource for the current tenant and returns it."
      x-rate-limit: { name: agentMutationLimiter }
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string, format: uuid }
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                overrides:
                  type: object
                  properties:
                    name: { type: string }
                    system_prompt: { type: string }
                    first_message: { type: string }
                    voice_id: { type: string }
      responses:
        '201': { description: Agent created from template }

  /api/onboarding/progress:
    get:
      tags: [Onboarding]
      summary: Get onboarding progress
      operationId: getOnboardingProgress
      description: "Get onboarding progress. Returns the current tenant's records, newest first, with pagination."
      responses:
        '200':
          description: Progress
          content:
            application/json:
              schema:
                type: object
                properties:
                  steps:
                    type: array
                    items:
                      type: object
                      properties:
                        step: { type: string }
                        completed: { type: boolean }
                        label: { type: string }
                  completionPercent: { type: integer }

  # ───────────────────────── Reports ─────────────────────────
  /api/reports:
    post:
      tags: [Reports]
      summary: Generate report
      operationId: postReports
      description: Generates an on-demand report. `date_from` and `date_to` may be either ISO datetimes or `YYYY-MM-DD` dates; maximum range is 1 year.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [type, period, date_from, date_to]
              properties:
                type:
                  type: string
                  enum: [roi_analysis, campaign_comparison, cost_per_call, agent_performance, contact_funnel]
                period:
                  type: string
                  enum: [daily, weekly, monthly, quarterly, custom]
                date_from: { type: string }
                date_to: { type: string }
                campaign_ids:
                  type: array
                  items: { type: string, format: uuid }
                format:
                  type: string
                  enum: [json, pdf]
                  default: json
      responses:
        '200':
          description: Report data
          content:
            application/json: {}
            application/pdf: {}
        '402': { $ref: '#/components/responses/PaymentRequired' }

  /api/reports/types:
    get:
      tags: [Reports]
      summary: List report types
      operationId: getReportsTypes
      description: "List report types. Returns the current tenant's records, newest first, with pagination."
      responses:
        '200': { description: Report types }

  # ───────────────────────── Legal ─────────────────────────
  /api/legal/accept-dpa:
    post:
      tags: [Legal]
      summary: Accept DPA / ToS
      operationId: postLegalAcceptDpa
      description: Records DPA / Terms of Service acceptance for the current tenant. Required before mutation endpoints unblock.
      responses:
        '200': { description: Recorded }

  /api/legal/dpa-status:
    get:
      tags: [Legal]
      summary: Get DPA acceptance status
      operationId: getLegalDpaStatus
      description: "Get DPA acceptance status. Returns the record for the current tenant."
      responses:
        '200':
          description: Status
          content:
            application/json:
              schema:
                type: object
                properties:
                  accepted: { type: boolean }
                  acceptedAt: { type: [string, "null"], format: date-time }

  # ───────────────────────── MCP Servers (workspace) ─────────────────────────
  /api/mcp-servers:
    get:
      tags: [MCP Servers]
      summary: List MCP servers
      operationId: getMcpServers
      description: "List MCP servers. Returns the current tenant's records, newest first, with pagination."
      responses:
        '200': { description: MCP servers }
    post:
      tags: [MCP Servers]
      summary: Create MCP server
      operationId: postMcpServers
      description: "Create MCP server. Creates the resource for the current tenant and returns it."
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name, serverUrl]
              properties:
                name: { type: string, minLength: 1, maxLength: 200 }
                description: { type: string, maxLength: 2000 }
                serverUrl: { type: string, format: uri }
                secretToken: { type: string, maxLength: 2000 }
                customHeaders:
                  type: object
                  additionalProperties: { type: string, maxLength: 2000 }
                approvalMode:
                  type: string
                  enum: [always_ask, fine_grained, no_approval]
                  default: always_ask
                toolOverrides:
                  type: object
                  additionalProperties:
                    type: string
                    enum: [auto_approved, requires_approval, disabled]
      responses:
        '201': { description: Created }
        '400': { $ref: '#/components/responses/BadRequest' }

  /api/mcp-servers/{mcpServerId}:
    get:
      tags: [MCP Servers]
      summary: Get MCP server
      operationId: getMcpServersByMcpServerId
      description: "Get MCP server. Returns the record for the current tenant."
      parameters:
        - $ref: '#/components/parameters/McpServerId'
      responses:
        '200': { description: MCP server }
        '404': { $ref: '#/components/responses/NotFound' }
    patch:
      tags: [MCP Servers]
      summary: Update MCP server
      operationId: patchMcpServersByMcpServerId
      description: "Update MCP server. Only the fields provided in the request body are changed."
      parameters:
        - $ref: '#/components/parameters/McpServerId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              description: Partial update.
      responses:
        '200': { description: Updated }
    delete:
      tags: [MCP Servers]
      summary: Delete MCP server
      operationId: deleteMcpServersByMcpServerId
      description: "Delete MCP server. Removes it from the current tenant."
      parameters:
        - $ref: '#/components/parameters/McpServerId'
      responses:
        '204': { description: Deleted }

  # ───────────────────────── Audit ─────────────────────────
  /api/audit:
    get:
      tags: [Audit]
      summary: Query audit log
      operationId: getAudit
      description: Admin-only. Filter by resource type, id, user id with pagination.
      x-rate-limit: { name: auditLimiter }
      parameters:
        - name: resourceType
          in: query
          schema: { type: string }
        - name: resourceId
          in: query
          schema: { type: string, format: uuid }
        - name: userId
          in: query
          schema: { type: string }
        - name: limit
          in: query
          schema: { type: integer, minimum: 1, maximum: 100, default: 50 }
        - name: offset
          in: query
          schema: { type: integer, minimum: 0, default: 0 }
      responses:
        '200': { description: Audit entries }
        '403': { $ref: '#/components/responses/Forbidden' }

  /api/audit/{id}:
    get:
      tags: [Audit]
      summary: Get audit entry
      operationId: getAuditById
      description: "Get audit entry. Returns the record for the current tenant."
      x-rate-limit: { name: auditLimiter }
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string, format: uuid }
      responses:
        '200': { description: Entry }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }

  # ───────────────────────── Compliance ─────────────────────────
  /api/compliance/consent:
    post:
      tags: [Compliance]
      summary: Grant consent
      operationId: postComplianceConsent
      description: "Grant consent. Creates the resource for the current tenant and returns it."
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [contactId, consentType, source]
              properties:
                contactId: { type: string, format: uuid }
                consentType:
                  type: string
                  enum: [voice_call, email, data_processing]
                source: { type: string, maxLength: 255 }
                evidence: { type: string, maxLength: 2000 }
                expiresAt: { type: string, format: date-time }
      responses:
        '201': { description: Granted }
    delete:
      tags: [Compliance]
      summary: Revoke consent
      operationId: deleteComplianceConsent
      description: "Revoke consent. Removes it from the current tenant."
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [contactId, consentType]
              properties:
                contactId: { type: string, format: uuid }
                consentType:
                  type: string
                  enum: [voice_call, email, data_processing]
      responses:
        '200': { description: Revoked }

  /api/compliance/consent/{contactId}:
    get:
      tags: [Compliance]
      summary: Get consent history
      operationId: getComplianceConsentByContactId
      description: "Get consent history. Returns the record for the current tenant."
      parameters:
        - name: contactId
          in: path
          required: true
          schema: { type: string, format: uuid }
      responses:
        '200': { description: History }

  /api/compliance/consent/check/{contactId}/{consentType}:
    get:
      tags: [Compliance]
      summary: Check consent presence
      operationId: getComplianceConsentCheckByContactIdByConsentType
      description: "Check consent presence. Returns the record for the current tenant."
      parameters:
        - name: contactId
          in: path
          required: true
          schema: { type: string, format: uuid }
        - name: consentType
          in: path
          required: true
          schema:
            type: string
            enum: [voice_call, email, data_processing]
      responses:
        '200':
          description: Consent state
          content:
            application/json:
              schema:
                type: object
                properties:
                  hasConsent: { type: boolean }

  /api/compliance/erase/{contactId}:
    post:
      tags: [Compliance]
      summary: GDPR erase contact
      operationId: postComplianceEraseByContactId
      description: Irrevocably erases PII for a contact across calls, transcripts, recordings and metadata.
      parameters:
        - name: contactId
          in: path
          required: true
          schema: { type: string, format: uuid }
      responses:
        '200': { description: Erased }

  /api/compliance/retention/enforce:
    post:
      tags: [Compliance]
      summary: Enforce retention policy
      operationId: postComplianceRetentionEnforce
      description: "Enforce retention policy."
      responses:
        '200': { description: Enforcement result }

  /api/compliance/configs:
    get:
      tags: [Compliance]
      summary: List country compliance configs
      operationId: getComplianceConfigs
      description: "List country compliance configs. Returns the current tenant's records, newest first, with pagination."
      responses:
        '200': { description: Configs }
    put:
      tags: [Compliance]
      summary: Upsert country compliance config
      operationId: putComplianceConfigs
      description: "Upsert country compliance config."
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
      responses:
        '200': { description: Saved }
        '402': { $ref: '#/components/responses/PaymentRequired' }

  /api/compliance/configs/{countryCode}:
    get:
      tags: [Compliance]
      summary: Get country compliance config
      operationId: getComplianceConfigsByCountryCode
      description: "Get country compliance config. Returns the record for the current tenant."
      parameters:
        - name: countryCode
          in: path
          required: true
          schema: { type: string, minLength: 2, maxLength: 2 }
      responses:
        '200': { description: Config }
        '404': { $ref: '#/components/responses/NotFound' }
    delete:
      tags: [Compliance]
      summary: Delete country compliance config
      operationId: deleteComplianceConfigsByCountryCode
      description: "Delete country compliance config. Removes it from the current tenant."
      parameters:
        - name: countryCode
          in: path
          required: true
          schema: { type: string, minLength: 2, maxLength: 2 }
      responses:
        '204': { description: Deleted }

  /api/compliance/defaults/{countryCode}:
    get:
      tags: [Compliance]
      summary: Get default compliance rules for country
      operationId: getComplianceDefaultsByCountryCode
      description: "Get default compliance rules for country. Returns the record for the current tenant."
      parameters:
        - name: countryCode
          in: path
          required: true
          schema: { type: string, minLength: 2, maxLength: 2 }
      responses:
        '200': { description: Defaults }

  /api/compliance/check-call:
    post:
      tags: [Compliance]
      summary: Pre-check outbound call legality
      operationId: postComplianceCheckCall
      description: Evaluates whether an outbound call to the target number is permitted given tenant compliance configs.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [phoneNumber]
              properties:
                phoneNumber: { type: string }
                tenantTimezone: { type: string }
      responses:
        '200': { description: Allowed / blocked with reason }

  # ───────────────────────── Billing ─────────────────────────
  /api/billing/checkout:
    post:
      tags: [Billing]
      summary: Create Stripe checkout session
      operationId: postBillingCheckout
      description: "Create Stripe checkout session. Creates the resource for the current tenant and returns it."
      x-rate-limit: { name: billingLimiter }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [plan]
              properties:
                plan:
                  type: string
                  enum: [starter, growth, business, enterprise]
                successUrl: { type: string, format: uri }
                cancelUrl: { type: string, format: uri }
      responses:
        '201':
          description: Checkout session
          content:
            application/json:
              schema:
                type: object
                properties:
                  url: { type: string, format: uri }

  /api/billing/portal:
    post:
      tags: [Billing]
      summary: Create Stripe customer portal session
      operationId: postBillingPortal
      description: "Create Stripe customer portal session. Creates the resource for the current tenant and returns it."
      x-rate-limit: { name: billingLimiter }
      responses:
        '201':
          description: Portal session
          content:
            application/json:
              schema:
                type: object
                properties:
                  url: { type: string, format: uri }

  /api/billing/subscription:
    get:
      tags: [Billing]
      summary: Get current subscription
      operationId: getBillingSubscription
      description: "Get current subscription. Returns the record for the current tenant."
      responses:
        '200': { description: Subscription }

  /api/billing/usage:
    get:
      tags: [Billing]
      summary: Get current usage
      operationId: getBillingUsage
      description: "Get current usage. Returns the record for the current tenant."
      responses:
        '200': { description: Usage counters }

  /api/billing/limits:
    get:
      tags: [Billing]
      summary: Get tier limits
      operationId: getBillingLimits
      description: "Get tier limits. Returns the record for the current tenant."
      responses:
        '200': { description: Limits }

  /api/billing/minute-packs:
    post:
      tags: [Billing]
      summary: Purchase minute pack
      operationId: postBillingMinutePacks
      description: "Purchase minute pack. Creates the resource for the current tenant and returns it."
      x-rate-limit: { name: billingLimiter }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [packSize]
              properties:
                packSize: { type: integer, enum: [500, 1000, 2500, 5000, 10000] }
      responses:
        '201': { description: Pack purchased }

  /api/billing/agent-addon:
    post:
      tags: [Billing]
      summary: Purchase agent add-on
      operationId: postBillingAgentAddon
      description: "Purchase agent add-on. Creates the resource for the current tenant and returns it."
      x-rate-limit: { name: billingLimiter }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [quantity]
              properties:
                quantity: { type: integer, minimum: 1 }
      responses:
        '201': { description: Add-on purchased }

  /api/billing/phone-number:
    post:
      tags: [Billing]
      summary: Purchase phone number
      operationId: postBillingPhoneNumber
      description: "Purchase phone number. Creates the resource for the current tenant and returns it."
      x-rate-limit: { name: billingLimiter }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [areaCode, country]
              properties:
                areaCode: { type: string }
                country: { type: string, minLength: 2, maxLength: 2 }
      responses:
        '201': { description: Number provisioned }

  # ───────────────────────── Public Widget ─────────────────────────
  /widget/init:
    post:
      tags: [Widgets]
      summary: Initialise widget session
      operationId: postWidgetInit
      description: Public endpoint called by the embedded JS widget. Authenticates with `x-widget-api-key` (NOT Clerk or `jl_`). IP-rate-limited.
      security: []
      parameters:
        - name: x-widget-api-key
          in: header
          required: true
          schema: { type: string }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [visitor_id]
              properties:
                visitor_id: { type: string }
                metadata: { type: object }
      responses:
        '200':
          description: Session created
          content:
            application/json:
              schema:
                type: object
                properties:
                  session_id: { type: string, format: uuid }

  /widget/message:
    post:
      tags: [Widgets]
      summary: Send widget message
      operationId: postWidgetMessage
      description: Sends a user message in an established widget session and returns the AI reply.
      security: []
      parameters:
        - name: x-widget-api-key
          in: header
          required: true
          schema: { type: string }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [session_id, message]
              properties:
                session_id: { type: string, format: uuid }
                message: { type: string, minLength: 1, maxLength: 4000 }
      responses:
        '200': { description: AI reply }

  /widget-embed/{id}/embed.js:
    get:
      tags: [Widgets]
      summary: Public widget embed script
      operationId: getWidgetEmbedByIdEmbedJs
      description: Anonymous — served to end-user browsers on the customer site. The UUID is the public credential.
      security: []
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string, format: uuid }
      responses:
        '200':
          description: JavaScript bundle
          content:
            application/javascript: {}

  # ───────────────────────── Inbound Webhooks (public, signature-validated) ─────────────────────────
  /webhooks/twilio/voice:
    post:
      tags: [Inbound Webhooks]
      summary: Twilio voice webhook
      operationId: postWebhooksTwilioVoice
      description: Called by Twilio for inbound call events; Twilio signature validated.
      security: []
      responses:
        '200': { description: TwiML }

  /webhooks/twilio/status:
    post:
      tags: [Inbound Webhooks]
      summary: Twilio status callback
      operationId: postWebhooksTwilioStatus
      description: "Twilio status callback."
      security: []
      responses:
        '200': { description: Acknowledged }

  /webhooks/twilio/queue-wait:
    post:
      tags: [Inbound Webhooks]
      summary: Twilio inbound queue wait
      operationId: postWebhooksTwilioQueueWait
      description: "Twilio inbound queue wait."
      security: []
      responses:
        '200': { description: TwiML }

  /webhooks/twilio/queue-connect:
    post:
      tags: [Inbound Webhooks]
      summary: Twilio inbound queue connect
      operationId: postWebhooksTwilioQueueConnect
      description: "Twilio inbound queue connect."
      security: []
      responses:
        '200': { description: TwiML }

  /webhooks/twilio/ivr-select:
    post:
      tags: [Inbound Webhooks]
      summary: Twilio IVR selection callback
      operationId: postWebhooksTwilioIvrSelect
      description: "Twilio IVR selection callback."
      security: []
      responses:
        '200': { description: TwiML }

  /webhooks/elevenlabs/post-call:
    post:
      tags: [Inbound Webhooks]
      summary: ElevenLabs post-call webhook
      operationId: postWebhooksElevenlabsPostCall
      description: "ElevenLabs post-call webhook."
      security: []
      responses:
        '200': { description: Acknowledged }

  /webhooks/whatsapp:
    post:
      tags: [Inbound Webhooks]
      summary: Inbound WhatsApp message webhook
      operationId: postWebhooksWhatsapp
      description: "Inbound WhatsApp message webhook."
      security: []
      responses:
        '200': { description: Acknowledged }

  /webhooks/email:
    post:
      tags: [Inbound Webhooks]
      summary: Inbound email webhook
      operationId: postWebhooksEmail
      description: "Inbound email webhook."
      security: []
      responses:
        '200': { description: Acknowledged }

  /webhooks/stripe:
    post:
      tags: [Inbound Webhooks]
      summary: Stripe webhook
      operationId: postWebhooksStripe
      description: Called by Stripe with signed events for subscription, invoice and payment lifecycle.
      security: []
      responses:
        '200': { description: Acknowledged }

  /webhooks/github:
    post:
      tags: [Inbound Webhooks]
      summary: GitHub webhook
      operationId: postWebhooksGithub
      description: "GitHub webhook."
      security: []
      responses:
        '200': { description: Acknowledged }

  /webhooks/integrations/{tenantId}/{integrationId}:
    post:
      tags: [Inbound Webhooks]
      summary: Inbound CRM webhook
      operationId: postWebhooksIntegrationsByTenantIdByIntegrationId
      description: Generic inbound webhook receiver for any CRM integration configured to push events.
      security: []
      parameters:
        - name: tenantId
          in: path
          required: true
          schema: { type: string, format: uuid }
        - name: integrationId
          in: path
          required: true
          schema: { type: string, format: uuid }
      responses:
        '200': { description: Accepted }

  /webhooks/integrations/oauth/callback:
    get:
      tags: [Inbound Webhooks]
      summary: OAuth callback
      operationId: getWebhooksIntegrationsOauthCallback
      description: Provider redirect endpoint — exchanges the authorization code for access tokens and redirects to the frontend.
      security: []
      parameters:
        - name: code
          in: query
          schema: { type: string }
        - name: state
          in: query
          schema: { type: string }
        - name: error
          in: query
          schema: { type: string }
        - name: error_description
          in: query
          schema: { type: string }
      responses:
        '302': { description: Redirect to frontend }

components:
  securitySchemes:
    ClerkBearer:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: |
        Clerk-issued JWT access token. Obtain via the Clerk SDK on the frontend and send it as
        `Authorization: Bearer <jwt>`. Any valid `/api/*` request accepts either this scheme or
        `ApiKeyBearer` — whichever header is present is used to resolve the tenant.
    ApiKeyBearer:
      type: http
      scheme: bearer
      description: |
        Tenant API key, prefixed with `jl_`, minted via `POST /api/api-keys`. Sent as
        `Authorization: Bearer jl_...`. Scoped to a single tenant and subject to the same RBAC and
        billing gates as a human admin, except that API-key callers cannot mint or revoke other
        API keys.
    ZapierKey:
      type: apiKey
      in: header
      name: x-zapier-api-key
      description: |
        Zapier-only credential (prefix `zk_`) minted via `POST /api/zapier/api-key`. Used only on
        the public `/zapier/*` trigger endpoints — Clerk and `jl_` keys are NOT accepted there.

  parameters:
    Page:
      name: page
      in: query
      description: 1-indexed page number.
      schema:
        type: integer
        minimum: 1
        default: 1
    Limit:
      name: limit
      in: query
      description: Maximum items per page.
      schema:
        type: integer
        minimum: 1
        maximum: 100
        default: 50
    AgentId:
      name: agentId
      in: path
      required: true
      schema: { type: string, format: uuid }
    CampaignId:
      name: campaignId
      in: path
      required: true
      schema: { type: string, format: uuid }
    CallId:
      name: callId
      in: path
      required: true
      schema: { type: string, format: uuid }
    IntegrationId:
      name: integrationId
      in: path
      required: true
      schema: { type: string, format: uuid }
    WebhookId:
      name: webhookId
      in: path
      required: true
      schema: { type: string, format: uuid }
    WidgetId:
      name: id
      in: path
      required: true
      schema: { type: string, format: uuid }
    McpServerId:
      name: mcpServerId
      in: path
      required: true
      schema: { type: string, format: uuid }

  responses:
    BadRequest:
      description: Invalid request payload.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/ErrorResponse' }
    Unauthorized:
      description: Missing or invalid credentials.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/ErrorResponse' }
    Forbidden:
      description: Authenticated but not authorised for this resource.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/ErrorResponse' }
    NotFound:
      description: Resource does not exist or does not belong to the tenant.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/ErrorResponse' }
    Conflict:
      description: State transition not permitted from the resource's current state.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/ErrorResponse' }
    PaymentRequired:
      description: Billing gate blocked the request — subscription, quota or add-on is missing.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/ErrorResponse' }
    RateLimited:
      description: Rate limit exceeded.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/ErrorResponse' }

  schemas:
    PaginationQuery:
      type: object
      properties:
        page:
          type: integer
          minimum: 1
          default: 1
        limit:
          type: integer
          minimum: 1
          maximum: 100
          default: 50

    ErrorResponse:
      type: object
      required: [error]
      properties:
        error:
          type: string
          description: Machine-readable error code.
          example: VALIDATION_FAILED
        message:
          type: string
          description: Human-readable description.
        statusCode:
          type: integer
        metadata:
          type: object
          description: Additional debug metadata (validation errors, conflicting fields).
          additionalProperties: true

    ApiKey:
      type: object
      properties:
        id: { type: string, format: uuid }
        name: { type: string, maxLength: 200 }
        prefix:
          type: string
          description: First 7 characters of the key for display (e.g. `jl_a1b`).
        scopes:
          type: array
          items: { type: string }
        created_at: { type: string, format: date-time }
        expires_at: { type: [string, "null"], format: date-time }
        last_used_at: { type: [string, "null"], format: date-time }
        revoked_at: { type: [string, "null"], format: date-time }

    CreatedApiKey:
      allOf:
        - $ref: '#/components/schemas/ApiKey'
        - type: object
          required: [plaintext]
          properties:
            plaintext:
              type: string
              description: Full key value (`jl_...`). Shown only once on creation — store it securely.
              example: "jl_1a2b3c4d5e6f7g8h9i0j..."

    Widget:
      type: object
      properties:
        id: { type: string, format: uuid }
        agent_id: { type: string, format: uuid }
        name: { type: string }
        allowed_origins:
          type: array
          items: { type: string, format: uri }
        branding:
          type: object
          properties:
            primary_color: { type: string, pattern: '^#[0-9a-fA-F]{6}$' }
            logo_url: { type: string, format: uri }
            position:
              type: string
              enum: [bottom-right, bottom-left]
            title: { type: string }
        voice_enabled: { type: boolean }
        greeting_message: { type: [string, "null"] }
        rate_limit_rpm: { type: integer, minimum: 1 }
        api_key:
          type: string
          description: Public widget key embedded in the customer's site.
        created_at: { type: string, format: date-time }

    CreateWidgetInput:
      type: object
      required: [agent_id, name, allowed_origins]
      properties:
        agent_id: { type: string, format: uuid }
        name: { type: string, minLength: 1, maxLength: 200 }
        allowed_origins:
          type: array
          minItems: 1
          items: { type: string, format: uri }
        branding:
          type: object
          required: [primary_color, position, title]
          properties:
            primary_color: { type: string, pattern: '^#[0-9a-fA-F]{6}$' }
            logo_url: { type: string, format: uri }
            position:
              type: string
              enum: [bottom-right, bottom-left]
            title: { type: string, maxLength: 100 }
        voice_enabled: { type: boolean, default: false }
        greeting_message: { type: string }
        rate_limit_rpm: { type: integer, minimum: 1, default: 30 }

    Agent:
      type: object
      properties:
        id: { type: string, format: uuid }
        tenantId: { type: string, format: uuid }
        name: { type: string }
        voiceId: { type: string }
        language:
          type: string
          enum: [es, es-CO, es-MX, es-AR, es-neutral, en, en-US, pt, pt-BR]
        systemPrompt: { type: string }
        firstMessage: { type: string }
        dynamicVariables:
          type: object
          additionalProperties: { type: string }
        objectionHandlers:
          type: object
          additionalProperties: { type: string }
        escalationRules:
          type: array
          items:
            type: object
            properties:
              trigger:
                type: string
                enum: [negative_sentiment, explicit_request, repeated_objection, timeout]
              threshold: { type: number }
              action:
                type: string
                enum: [transfer_to_human, end_call_politely, schedule_callback]
        maxCallDurationSeconds: { type: integer, minimum: 30, maximum: 3600 }
        transferPhoneNumber:
          type: [string, "null"]
          pattern: '^\+[1-9]\d{1,14}$'
        enableVoicemailDetection: { type: boolean }
        department: { type: [string, "null"] }
        skills:
          type: array
          items: { type: string }
        expressiveMode: { type: boolean }
        toolCallSoundEnabled: { type: boolean }
        textNormalisationType:
          type: string
          enum: [system_prompt, elevenlabs]
        turnTimeout: { type: number }
        turnEagerness:
          type: string
          enum: [low, medium, high, eager]
        ttsSpeed: { type: number }
        ttsStability: { type: number }
        ttsSimilarityBoost: { type: number }
        createdAt: { type: string, format: date-time }
        updatedAt: { type: string, format: date-time }

    CreateAgentInput:
      type: object
      required: [name, voiceId, language, systemPrompt]
      properties:
        name: { type: string, minLength: 1, maxLength: 100 }
        voiceId: { type: string, minLength: 1 }
        language:
          type: string
          enum: [es, es-CO, es-MX, es-AR, es-neutral, en, en-US, pt, pt-BR]
        systemPrompt: { type: string, minLength: 10, maxLength: 5000 }
        firstMessage: { type: string, minLength: 1, maxLength: 500 }
        dynamicVariables:
          type: object
          additionalProperties: { type: string }
        objectionHandlers:
          type: object
          additionalProperties: { type: string }
        escalationRules:
          type: array
          items:
            type: object
            properties:
              trigger:
                type: string
                enum: [negative_sentiment, explicit_request, repeated_objection, timeout]
              threshold: { type: number }
              action:
                type: string
                enum: [transfer_to_human, end_call_politely, schedule_callback]
        maxCallDurationSeconds:
          type: integer
          minimum: 30
          maximum: 3600
          default: 600
        transferPhoneNumber:
          type: string
          pattern: '^\+[1-9]\d{1,14}$'
        enableVoicemailDetection: { type: boolean, default: true }
        department: { type: string, maxLength: 100 }
        skills:
          type: array
          items: { type: string, maxLength: 100 }
          maxItems: 50
        safety:
          type: object
          properties:
            blockedInputTopics:
              type: array
              items: { type: string, maxLength: 200 }
            blockedOutputTopics:
              type: array
              items: { type: string, maxLength: 200 }
        privacy:
          type: object
          properties:
            redactPiiAudio: { type: boolean, default: true }
            deleteAudioAfterProcessing: { type: boolean, default: true }
            retentionDays:
              type: integer
              minimum: 0
              maximum: 365
              default: 90
        analysis:
          type: object
          properties:
            successEvaluation:
              type: array
              items:
                type: object
                required: [id, description]
                properties:
                  id: { type: string, maxLength: 100 }
                  description: { type: string, maxLength: 1000 }
            dataCollection:
              type: array
              items:
                type: object
                required: [id, dataType, description]
                properties:
                  id: { type: string, maxLength: 100 }
                  dataType:
                    type: string
                    enum: [string, boolean, integer, number]
                  description: { type: string, maxLength: 1000 }
        expressiveMode: { type: boolean, default: false }
        toolCallSoundEnabled: { type: boolean, default: false }
        textNormalisationType:
          type: string
          enum: [system_prompt, elevenlabs]
          default: elevenlabs
        turnTimeout:
          type: number
          minimum: 1
          maximum: 30
          default: 7
        turnEagerness:
          type: string
          enum: [low, medium, high, eager]
          default: eager
        ttsSpeed:
          type: number
          minimum: 0.5
          maximum: 2.0
          default: 1.1
        ttsStability:
          type: number
          minimum: 0
          maximum: 1
          default: 0.5
        ttsSimilarityBoost:
          type: number
          minimum: 0
          maximum: 1
          default: 0.75

    UpdateAgentInput:
      description: Partial update — any subset of `CreateAgentInput` fields, at least one required.
      type: object
      additionalProperties: true

    CreateToolInput:
      type: object
      required: [name, description, url]
      properties:
        name:
          type: string
          pattern: '^[a-z][a-z0-9_]*$'
          minLength: 1
          maxLength: 100
        description: { type: string, minLength: 5, maxLength: 2000 }
        type:
          type: string
          enum: [webhook]
          default: webhook
        method:
          type: string
          enum: [GET, POST, PUT, PATCH, DELETE]
          default: GET
        url: { type: string, format: uri, maxLength: 2000 }
        headers: { type: array, items: { type: object } }
        pathParams: { type: array, items: { type: object } }
        queryParams: { type: array, items: { type: object } }
        bodyParams: { type: array, items: { type: object } }
        authType:
          type: string
          enum: [none, bearer, basic, oauth2_client_credentials, oauth2_jwt, custom_header]
          default: none
        authConfig:
          type: object
          additionalProperties: true
        responseTimeoutSecs:
          type: integer
          minimum: 1
          maximum: 120
          default: 20
        toolCallSound:
          type: string
          enum: [ringing, typing, ambient]
        toolCallSoundBehavior:
          type: string
          enum: [auto, always, never]
          default: auto
        dynamicVariableAssignments:
          type: array
          items: { type: object }
        position:
          type: integer
          minimum: 0
          default: 0

    Campaign:
      type: object
      properties:
        id: { type: string, format: uuid }
        tenantId: { type: string, format: uuid }
        agentId: { type: string, format: uuid }
        name: { type: string }
        productContext: { type: string }
        targetAudience: { type: string }
        status:
          type: string
          enum: [draft, active, paused, completed, archived]
        channel:
          type: string
          enum: [voice, whatsapp, webchat, email]
        category:
          type: string
          enum: [sales, support, scheduling, survey, general]
        maxConcurrentCalls: { type: integer, minimum: 1, maximum: 500 }
        maxRetryAttempts: { type: integer, minimum: 0, maximum: 10 }
        retryIntervalMinutes: { type: integer, minimum: 5, maximum: 1440 }
        schedule:
          $ref: '#/components/schemas/CampaignSchedule'
        createdAt: { type: string, format: date-time }
        updatedAt: { type: string, format: date-time }

    CampaignSchedule:
      type: object
      required: [timezone, days]
      properties:
        timezone:
          type: string
          example: America/Bogota
        days:
          type: array
          minItems: 1
          items:
            type: object
            required: [day, startHour, endHour, enabled]
            properties:
              day:
                type: string
                enum: [monday, tuesday, wednesday, thursday, friday, saturday, sunday]
              startHour: { type: integer, minimum: 0, maximum: 23 }
              endHour: { type: integer, minimum: 1, maximum: 24 }
              enabled: { type: boolean }

    CreateCampaignInput:
      type: object
      required: [agentId, name, productContext, targetAudience, schedule]
      properties:
        agentId: { type: string, format: uuid }
        name: { type: string, minLength: 1, maxLength: 200 }
        productContext: { type: string, minLength: 10, maxLength: 5000 }
        targetAudience: { type: string, minLength: 1, maxLength: 1000 }
        schedule: { $ref: '#/components/schemas/CampaignSchedule' }
        maxConcurrentCalls:
          type: integer
          minimum: 1
          maximum: 500
          default: 10
        maxRetryAttempts:
          type: integer
          minimum: 0
          maximum: 10
          default: 3
        retryIntervalMinutes:
          type: integer
          minimum: 5
          maximum: 1440
          default: 60
        channel:
          type: string
          enum: [voice, whatsapp, webchat, email]
          default: voice
        category:
          type: string
          enum: [sales, support, scheduling, survey, general]
          default: sales

    UpdateCampaignInput:
      description: Partial update — any subset of `CreateCampaignInput` fields.
      type: object
      additionalProperties: true

    Contact:
      type: object
      properties:
        id: { type: string, format: uuid }
        campaignId: { type: string, format: uuid }
        phoneNumber:
          type: [string, "null"]
          pattern: '^\+[1-9]\d{1,14}$'
        email:
          type: [string, "null"]
          format: email
        whatsappNumber:
          type: [string, "null"]
          pattern: '^\+[1-9]\d{1,14}$'
        name:
          type: [string, "null"]
        status:
          type: string
          enum: [pending, in_progress, completed, failed, do_not_call]
        metadata:
          type: object
          additionalProperties: { type: string }
        createdAt: { type: string, format: date-time }

    CreateContactInput:
      type: object
      properties:
        phoneNumber:
          type: string
          pattern: '^\+[1-9]\d{1,14}$'
        email:
          type: string
          format: email
          maxLength: 320
        whatsappNumber:
          type: string
          pattern: '^\+[1-9]\d{1,14}$'
        name:
          type: string
          maxLength: 200
        metadata:
          type: object
          additionalProperties: { type: string, maxLength: 500 }
          description: Up to 20 keys, alphanumeric + underscore/dash only.

    InitiateCallInput:
      type: object
      required: [agentId]
      description: Either `phoneNumber` or `contactId` is required in addition to `agentId`.
      properties:
        phoneNumber:
          type: string
          pattern: '^\+[1-9]\d{1,14}$'
          example: "+573001234567"
        agentId: { type: string, format: uuid }
        campaignId: { type: string, format: uuid }
        contactId: { type: string, format: uuid }
        promptVariantId: { type: string, format: uuid }
        promptOverride: { type: string, maxLength: 5000 }
        firstMessageOverride: { type: string, maxLength: 500 }

    Call:
      type: object
      properties:
        id: { type: string, format: uuid }
        tenantId: { type: string, format: uuid }
        agentId: { type: string, format: uuid }
        campaignId:
          type: [string, "null"]
          format: uuid
        contactId:
          type: [string, "null"]
          format: uuid
        phoneNumber: { type: string }
        direction:
          type: string
          enum: [inbound, outbound]
        status:
          type: string
          enum: [queued, ringing, in_progress, completed, failed, no_answer, busy, voicemail, cancelled]
        outcome:
          type: [string, "null"]
        durationSeconds:
          type: [integer, "null"]
        sentimentScore:
          type: [number, "null"]
          minimum: -1
          maximum: 1
        recordingUrl:
          type: [string, "null"]
          format: uri
        transcriptUrl:
          type: [string, "null"]
          format: uri
        startedAt:
          type: [string, "null"]
          format: date-time
        endedAt:
          type: [string, "null"]
          format: date-time
        createdAt: { type: string, format: date-time }

    Webhook:
      type: object
      properties:
        id: { type: string, format: uuid }
        url: { type: string, format: uri }
        events:
          type: array
          items: { type: string }
        description:
          type: [string, "null"]
        is_active: { type: boolean }
        filters:
          type: [object, "null"]
          properties:
            agentIds:
              type: array
              items: { type: string, format: uuid }
            outcomes:
              type: array
              items: { type: string }
            minSentiment:
              type: number
              minimum: -1
              maximum: 1
            conditions:
              type: array
              items:
                type: object
                required: [field, operator]
                properties:
                  field: { type: string }
                  operator:
                    type: string
                    enum: [eq, neq, gt, lt, gte, lte, contains, exists]
                  value: {}
        headers:
          type: [object, "null"]
          additionalProperties: { type: string }
        payload_template:
          type: [string, "null"]
        secret:
          type: string
          description: Masked after creation — use `rotate-secret` to regenerate.
          example: "[configured]"
        created_at: { type: string, format: date-time }

    CreateWebhookInput:
      type: object
      required: [url, events]
      properties:
        url: { type: string, format: uri, maxLength: 2000 }
        events:
          type: array
          minItems: 1
          items: { type: string }
          example: ["call.completed", "call.failed"]
        description: { type: string, maxLength: 500 }
        filters:
          type: object
          additionalProperties: true
        headers:
          type: object
          additionalProperties: { type: string, maxLength: 2000 }
        payload_template: { type: string, maxLength: 10000 }

    Conversation:
      type: object
      properties:
        id: { type: string, format: uuid }
        tenantId: { type: string, format: uuid }
        agentId: { type: string, format: uuid }
        contactId:
          type: [string, "null"]
          format: uuid
        channel:
          type: string
          enum: [voice, whatsapp, webchat, email]
        status:
          type: string
          enum: [active, closed, archived]
        lastMessageAt:
          type: [string, "null"]
          format: date-time
        createdAt: { type: string, format: date-time }

    Message:
      type: object
      properties:
        id: { type: string, format: uuid }
        conversationId: { type: string, format: uuid }
        role:
          type: string
          enum: [user, assistant, system, tool]
        content: { type: string }
        createdAt: { type: string, format: date-time }
        metadata:
          type: object
          additionalProperties: true
