openapi: 3.1.0
info:
  title: Perpetuus API
  version: "1.0"
  summary: API REST oficial do Perpetuus CRM
  description: |
    A **Perpetuus API** dá acesso programático a tudo que você vê no CRM: boards,
    cards, contatos, segmentos, campanhas, tarefas, orçamentos e mais.

    ## Base URL

    ```
    https://api.perpetuus.com.br/api
    ```

    ## Leve para a IDE

    Baixe o contrato OpenAPI, a documentação completa e o JSON do MCP em
    [Leve para a IDE](/docs/api/para-ide).

    ## Autenticação

    Toda requisição (exceto login e formulários públicos) exige **dois headers**:

    | Header | Valor | Descrição |
    |--------|-------|-----------|
    | `Authorization` | `Bearer SEU_TOKEN` | Token de API ou JWT de sessão |
    | `x-tenant-id` | `slug-da-organizacao` | Identifica a sua organização (ex.: `acme`) |

    Sem `x-tenant-id` a API responde `400`; sem token válido, `401`; sem permissão, `403`.

    ## Identificadores

    Use sempre o **documentId** (string) dos recursos nas URLs — nunca o `id` numérico interno.

    ## Envelopes de resposta

    - **Listas paginadas** (cards, contatos, boards): `{ "data": [...], "meta": { "pagination": {...} } }`
    - **Objeto único** (get/create/update de board, card e contato): o objeto **direto**, sem envelope
    - **Relatórios**: objeto flat próprio (ver `GET /boards/{boardId}/report`)
  contact:
    name: Suporte Perpetuus
    url: https://perpetuus.com.br
    email: luis.santos@perpetuus.com.br
  x-logo:
    url: /logo.svg
servers:
  - url: https://api.perpetuus.com.br/api
    description: Produção
security:
  - bearerAuth: []
tags:
  - name: Autenticação
    description: Login e geração de tokens de API.
  - name: Boards
    description: Funis/quadros do CRM. Um board tem fases, campos, tags e cards.
  - name: Membros do board
    description: Gestão de quem vê e edita cada board.
  - name: Cards
    description: Oportunidades/negócios dentro de um board. Sempre acessados via rota nested `/boards/{boardId}/cards`.
  - name: Cards vinculados
    description: Vínculos entre cards de boards diferentes (ex. venda → onboarding).
  - name: Fases
    description: Colunas do board. A Fase 0 é o formulário de entrada.
  - name: Campos
    description: Campos dinâmicos configuráveis por fase (texto, e-mail, moeda, select…).
  - name: Condições de campo
    description: Regras que exibem/ocultam campos conforme valores de outros campos.
  - name: Tags
    description: Etiquetas de card, escopadas por board.
  - name: Relatórios
    description: Analytics agregados do funil (conversão, ganhos/perdas, breakdown por fase).
  - name: Formulário público
    description: Endpoints públicos (sem autenticação) para captura de leads via formulário da Fase 0.
  - name: Contatos
    description: Pessoas e empresas. Suporta campos customizados e operações em massa.
  - name: Atividades de contato
    description: Timeline de eventos do contato (notas, cards criados, e-mails enviados…).
  - name: Segmentos
    description: Audiências dinâmicas de contatos definidas por regras de filtro.
  - name: Campanhas
    description: Disparos em massa (WhatsApp/e-mail) para segmentos, com estatísticas de entrega.
  - name: Tarefas
    description: Lembretes e follow-ups vinculados a cards e boards.
  - name: Comentários
    description: Comentários em cards, com suporte a menções.
  - name: Orçamentos
    description: Propostas comerciais (quotes) com itens, produtos e geração de PDF.
  - name: Produtos
    description: Catálogo de produtos e categorias.
  - name: Times
    description: Times comerciais para roteamento e escopo de carteira.
  - name: Modelos de PDF
    description: Templates de PDF usados em orçamentos e documentos.
  - name: Automações
    description: |
      **BETA.** Workflows do CRM (triggers, ações, publish, runs). Contratos podem mudar;
      publish/test têm efeitos colaterais.
  - name: Canais
    description: |
      **BETA.** Canais de mensagem (WhatsApp, e-mail, etc.), templates e vínculo com boards.
      Operações de template alteram o provedor externo.
  - name: Meta Lead Ads
    description: |
      **BETA.** Integrações Lead Ads, OAuth e roteamento de formulários para boards.
      Tokens e webhooks são sensíveis.
  - name: E-mail marketing
    description: |
      **BETA.** Templates e assets de e-mail. `send-test` envia mensagem real.
  - name: Distribuição
    description: |
      **BETA.** Filas de distribuição automática de cards entre membros.
  - name: Filtros salvos
    description: |
      **BETA.** Filtros salvos de board (pessoais ou compartilhados).
  - name: Conversões
    description: |
      **BETA.** Pixels / CAPI (Meta, GA4). Create/verify chamam APIs externas.
components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: |
        Token de API (recomendado para integrações) ou JWT de sessão.
        Gere o seu em **Configurações → Organização → aba API** ou via
        `POST /users/generate-token`.
  parameters:
    TenantHeader:
      name: x-tenant-id
      in: header
      required: true
      description: Slug da sua organização (ex.:`acme`). Visível na URL do app e em Configurações.
      schema:
        type: string
        example: acme
    PaginationPage:
      name: pagination[page]
      in: query
      description: Página (começa em 1).
      schema:
        type: integer
        default: 1
        minimum: 1
    PaginationPageSize:
      name: pagination[pageSize]
      in: query
      description: Itens por página. Máximo **100**.
      schema:
        type: integer
        default: 25
        minimum: 1
        maximum: 100
    Sort:
      name: sort
      in: query
      description: "Ordenação no formato `campo:asc|desc` (ex.: `updatedAt:desc`)."
      schema:
        type: string
        example: updatedAt:desc
    Populate:
      name: populate
      in: query
      description: |
        Relações a incluir na resposta. Aceita notação de array:
        `populate[0]=assignees&populate[1]=tags`.
      schema:
        type: string
      style: form
      explode: true
  schemas:
    Error:
      type: object
      description: Envelope padrão de erro.
      properties:
        data:
          type: "null"
        error:
          type: object
          properties:
            status:
              type: integer
              example: 400
            name:
              type: string
              example: BadRequestError
            message:
              type: string
              example: Header x-tenant-id é obrigatório
            details:
              type: object
              description: Detalhes adicionais (ex. erros de validação por campo).
    Pagination:
      type: object
      properties:
        page:
          type: integer
          example: 1
        pageSize:
          type: integer
          example: 25
        pageCount:
          type: integer
          example: 4
        total:
          type: integer
          example: 87
    CardStats:
      type: object
      description: Estatísticas agregadas da listagem de cards (presente por padrão; desligue com `stats=false`).
      properties:
        overdueCount:
          type: integer
          description: Cards com `dueDate` anterior ao início do dia (UTC, ou `statsTodayStart` se enviado).
          example: 3
        valueSum:
          type: number
          description: Soma de `value` de todos os cards que casam com o filtro (não só a página atual).
          example: 125000.5
        phaseSlaBreachedCount:
          type: integer
          description: Cards que estouraram o SLA da fase. Só é calculado quando o filtro fixa uma `currentPhase` com SLA configurado; caso contrário é `0`.
          example: 1
    UserRef:
      type: object
      properties:
        username:
          type: string
          example: luis
        email:
          type: string
          example: luis@acme.com
        blocked:
          type: boolean
          example: false
    MembershipRef:
      type: object
      description: Membro da organização (membership). Use o `documentId` em `assignees`, `owner` etc.
      properties:
        documentId:
          type: string
          example: mb1a2b3c
        user:
          $ref: "#/components/schemas/UserRef"
    Tag:
      type: object
      properties:
        documentId:
          type: string
          example: tg1a2b3c
        name:
          type: string
          example: VIP
        slug:
          type: string
          example: vip
        color:
          type: string
          description: Cor em hex (`#RGB` ou `#RRGGBB`).
          example: "#3b82f6"
    FieldDefinition:
      type: object
      description: Definição de um campo dinâmico de fase.
      properties:
        documentId:
          type: string
          example: fl1a2b3c
        label:
          type: string
          example: E-mail
        slug:
          type: string
          description: Identificador usado como chave em `fields` na escrita/leitura de cards.
          example: email
        type:
          type: string
          enum:
            - text
            - textarea
            - richtext
            - email
            - number
            - date
            - datetime
            - select
            - multiselect
            - radio
            - switch
            - file
            - url
            - phone
            - card_link
            - hidden
            - statement
            - time
            - currency
            - document
            - checkbox
          example: email
        order:
          type: integer
          example: 1
        required:
          type: boolean
          example: true
        placeholder:
          type: string
        helpText:
          type: string
        defaultValue:
          type: string
        options:
          description: Opções para `select`, `multiselect` e `radio`. Array de strings ou de `{ value, label }`.
          type: array
          items: {}
    AllowedTransition:
      type: object
      description: Transição permitida entre fases (aplicada na interface Kanban).
      properties:
        phase:
          type: object
          properties:
            documentId:
              type: string
        allowed:
          type: boolean
          default: true
    Phase:
      type: object
      description: Fase (coluna) de um board. A fase de `order` 0 é o formulário de entrada.
      properties:
        documentId:
          type: string
          example: ph1a2b3c
        name:
          type: string
          example: Qualificação
        slug:
          type: string
          example: qualificacao
        order:
          type: integer
          minimum: 0
          example: 2
        isPublic:
          type: boolean
          description: Fases `order` 0 são sempre públicas (formulário).
        description:
          type: string
        maxTimeInPhaseHours:
          type:
            - integer
            - "null"
          description: SLA da fase em horas (1–8760). `null` = sem SLA.
          example: 48
        fields:
          type: array
          items:
            $ref: "#/components/schemas/FieldDefinition"
        allowedTransitions:
          type: array
          items:
            $ref: "#/components/schemas/AllowedTransition"
    BoardSettings:
      type: object
      description: Configurações do board.
      properties:
        showValue:
          type: boolean
          description: Exibe/calcula valores monetários dos cards.
        currency:
          type: string
          example: BRL
        wonPhase:
          type:
            - string
            - "null"
          description: documentId ou slug da fase de "ganho".
        lostPhase:
          type:
            - string
            - "null"
          description: documentId ou slug da fase de "perda".
        restrictEditToAssignees:
          type: boolean
        restrictViewToAssignees:
          type: boolean
        enableProducts:
          type: boolean
        autoUpdateCardValue:
          type: boolean
        autoUpdateDueDateFromPhase:
          type: boolean
    Board:
      type: object
      properties:
        documentId:
          type: string
          example: bd1a2b3c
        name:
          type: string
          example: Funil de Vendas
        slug:
          type: string
          example: funil-de-vendas
        active:
          type: boolean
        boardType:
          type: string
          enum:
            - kanban
            - chat
            - hybrid
          default: kanban
        icon:
          type: string
          example: layout-grid
        color:
          type: string
        template:
          type: string
          example: minimal-workflow
        taskStatuses:
          type: array
          items:
            type: string
          example:
            - pending
            - in-progress
            - completed
        boardSettings:
          $ref: "#/components/schemas/BoardSettings"
        phases:
          type: array
          items:
            $ref: "#/components/schemas/Phase"
        tags:
          type: array
          items:
            $ref: "#/components/schemas/Tag"
        owner:
          $ref: "#/components/schemas/MembershipRef"
        members:
          type: array
          items:
            $ref: "#/components/schemas/MembershipRef"
        editors:
          type: array
          items:
            $ref: "#/components/schemas/MembershipRef"
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
    ContactRef:
      type: object
      properties:
        documentId:
          type: string
          example: ct1a2b3c
        name:
          type: string
          example: João Silva
        email:
          type: string
          example: joao@cliente.com
        phone:
          type: string
          example: "+5511999999999"
    FieldDataItem:
      type: object
      description: |
        Item cru de `fieldData[]` — detalhe avançado por fase. O storage interno de
        valores escalares usa `{ "val": <valor> }`. Para a maioria dos casos, prefira
        o mapa achatado `fields`.
      properties:
        documentId:
          type: string
        value:
          description: 'Valor armazenado. Escalares vêm embrulhados: `{ "val": "joao@x.com" }`.'
          type: object
        field:
          $ref: "#/components/schemas/FieldDefinition"
        phase:
          type: object
          properties:
            documentId:
              type: string
            name:
              type: string
    Card:
      type: object
      description: |
        Card (oportunidade/negócio). Nas respostas, além de `fieldData[]` (cru), o
        card traz **`fields`** — um mapa `slug → valor` já desembrulhado, ideal para
        integrações e BI.
      properties:
        documentId:
          type: string
          example: cd1a2b3c
        title:
          type: string
          example: João Silva
        description:
          type: string
          description: Texto rico (HTML).
        value:
          type: number
          example: 15000.5
        dueDate:
          type:
            - string
            - "null"
          format: date-time
        dueDateSource:
          type: string
          enum:
            - manual
            - phase
            - automation
        currentPhase:
          $ref: "#/components/schemas/Phase"
        currentPhaseEnteredAt:
          type: string
          format: date-time
        assignees:
          type: array
          items:
            $ref: "#/components/schemas/MembershipRef"
        tags:
          type: array
          items:
            $ref: "#/components/schemas/Tag"
        contact:
          $ref: "#/components/schemas/ContactRef"
        fields:
          type: object
          description: Mapa `slug → valor` com todos os campos dinâmicos preenchidos (leitura preferida).
          additionalProperties: true
          example:
            name: João Silva
            email: joao@cliente.com
            valor_orcamento: 25000
        fieldData:
          type: array
          items:
            $ref: "#/components/schemas/FieldDataItem"
        creationSource:
          type: string
          enum:
            - manual
            - form
            - meta_lead
            - automation
            - chatwoot_inbound
            - import
            - api
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
    Contact:
      type: object
      description: Contato (pessoa ou empresa). `customFields` é um objeto `chave → valor`.
      properties:
        documentId:
          type: string
          example: ct1a2b3c
        name:
          type: string
          example: João Silva
        email:
          type:
            - string
            - "null"
          example: joao@cliente.com
        phone:
          type:
            - string
            - "null"
          example: "+5511999999999"
        contact_type:
          type: string
          enum:
            - pessoa
            - empresa
          default: pessoa
        cpf:
          type:
            - string
            - "null"
          description: Validado e armazenado sem máscara. Único por organização.
        cnpj:
          type:
            - string
            - "null"
          description: Validado e armazenado sem máscara. Único por organização.
        address:
          type:
            - string
            - "null"
        city:
          type:
            - string
            - "null"
        state:
          type:
            - string
            - "null"
        country:
          type:
            - string
            - "null"
        postal_code:
          type:
            - string
            - "null"
        birthdate:
          type:
            - string
            - "null"
          format: date
        first_buy_date:
          type:
            - string
            - "null"
          format: date
        owner:
          $ref: "#/components/schemas/MembershipRef"
        ownedAt:
          type:
            - string
            - "null"
          format: date-time
        customFields:
          type: object
          additionalProperties: true
          example:
            origem: Indicação
            nps: 9
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
    ContactActivity:
      type: object
      description: Evento na timeline de um contato.
      properties:
        documentId:
          type: string
        activityType:
          type: string
          enum:
            - contact_created
            - contact_updated
            - owner_assigned
            - owner_changed
            - team_assigned
            - team_changed
            - card_created
            - card_won
            - card_lost
            - email_sent
            - whatsapp_sent
            - note_added
            - call_completed
            - call_attempted
        description:
          type: string
          example: Cliente pediu retorno na sexta.
        performedAt:
          type: string
          format: date-time
        contact:
          $ref: "#/components/schemas/ContactRef"
        actor:
          $ref: "#/components/schemas/MembershipRef"
        metadata:
          type: object
          additionalProperties: true
    SegmentFilters:
      type: object
      description: Regras de um segmento.
      required:
        - logic
        - conditions
      properties:
        logic:
          type: string
          enum:
            - AND
            - OR
        conditions:
          type: array
          minItems: 1
          items:
            type: object
            description: |
              Condição de filtro. Campos string: `phone, email, name, address, city, state,
              country, postal_code, cpf, cnpj, contact_type`. Operadores: `contains, eq,
              startsWith, endsWith, empty, notEmpty`. Relações usam `kind: "relation"`
              (field `owner|team` + `documentId`); campos customizados usam `kind: "custom"`
              (`slug` + `op` + `value`).
            properties:
              field:
                type: string
                example: city
              op:
                type: string
                example: eq
              value:
                example: São Paulo
              kind:
                type: string
                enum:
                  - relation
                  - custom
              slug:
                type: string
              documentId:
                type: string
      example:
        logic: AND
        conditions:
          - field: city
            op: eq
            value: São Paulo
          - field: email
            op: notEmpty
    Segment:
      type: object
      properties:
        documentId:
          type: string
          example: sg1a2b3c
        name:
          type: string
          example: Leads SP com e-mail
        description:
          type:
            - string
            - "null"
        active:
          type: boolean
        filters:
          $ref: "#/components/schemas/SegmentFilters"
        contactCount:
          type: integer
          description: Contagem de contatos que casam com as regras.
          example: 231
    CampaignStats:
      type: object
      description: Contadores agregados da campanha. Engajamento de e-mail (`delivered/opened/clicked/bounced`) vem da ingestão de eventos do provedor.
      properties:
        total:
          type: integer
          example: 500
        pending:
          type: integer
          example: 120
        sent:
          type: integer
          example: 350
        failed:
          type: integer
          example: 20
        skipped:
          type: integer
          example: 10
        delivered:
          type: integer
          example: 300
        opened:
          type: integer
          example: 180
        clicked:
          type: integer
          example: 45
        bounced:
          type: integer
          example: 5
    Task:
      type: object
      properties:
        documentId:
          type: string
          example: tk1a2b3c
        name:
          type: string
          maxLength: 255
          example: Ligar para o cliente
        description:
          type: string
          description: Texto rico (HTML).
        dueDate:
          type:
            - string
            - "null"
          format: date-time
        type:
          type: string
          enum:
            - reminder
            - follow-up
            - action
            - other
          default: reminder
        taskStatus:
          type: string
          description: Um dos status configurados no board (`taskStatuses`).
          example: pending
        completedAt:
          type:
            - string
            - "null"
          format: date-time
        card:
          type: object
          properties:
            documentId:
              type: string
            title:
              type: string
        assignees:
          type: array
          items:
            $ref: "#/components/schemas/MembershipRef"
    BoardReport:
      type: object
      description: Relatório agregado do board (envelope flat, sem `{ data, meta }`).
      properties:
        boardId:
          type: string
          example: bd1a2b3c
        boardName:
          type: string
          example: Funil de Vendas
        showValue:
          type: boolean
        currency:
          type: string
          example: BRL
        dateRange:
          type: object
          properties:
            from:
              type: string
              format: date-time
            to:
              type: string
              format: date-time
        summary:
          type: object
          properties:
            totalCards:
              type: integer
              example: 42
            totalValue:
              type: number
              example: 128000
            wonCount:
              type: integer
              example: 8
            lostCount:
              type: integer
              example: 5
            wonValue:
              type: number
              example: 64000
            opportunityValue:
              type: number
              example: 40000
            unassignedCount:
              type: integer
              example: 3
            untaggedCount:
              type: integer
              example: 10
        phases:
          type: array
          items:
            type: object
            properties:
              phaseId:
                type: string
                description: documentId da fase, ou `"__no_phase__"` para cards sem fase.
              name:
                type: string
              slug:
                type: string
              order:
                type: integer
              count:
                type: integer
              value:
                type: number
              isWon:
                type: boolean
              isLost:
                type: boolean
        assignees:
          type: array
          items:
            type: object
            properties:
              membershipId:
                type:
                  - string
                  - "null"
                description: '`null` para a linha "Sem responsável".'
              displayName:
                type: string
              count:
                type: integer
              value:
                type: number
        tags:
          type: array
          items:
            type: object
            properties:
              tagId:
                type: string
              name:
                type: string
              color:
                type: string
              count:
                type: integer
        createdByDay:
          type: array
          items:
            type: object
            properties:
              date:
                type: string
                format: date
                example: 2026-07-01
              count:
                type: integer
    FieldCondition:
      type: object
      description: Regra de exibição condicional de campos.
      properties:
        documentId:
          type: string
        name:
          type: string
          example: Mostrar CNPJ para empresas
        scope:
          type: string
          enum:
            - phase
            - board
        order:
          type: integer
        enabled:
          type: boolean
        criteria:
          type: array
          description: 'Grupos `{ operator: "AND", expressions: [{ fieldSlug, operator, value }] }`.'
          items:
            type: object
        actionsWhenTrue:
          type: array
          description: 'Ações `{ type: "show"|"hide", targetFieldSlugs: [...] }`.'
          items:
            type: object
        actionsWhenFalse:
          type: array
          items:
            type: object
        phase:
          type: object
          properties:
            documentId:
              type: string
            name:
              type: string
    Campaign:
      type: object
      properties:
        documentId:
          type: string
          example: cp1a2b3c
        name:
          type: string
          example: Reativação julho
        description:
          type:
            - string
            - "null"
        channel:
          type: string
          enum:
            - whatsapp
            - email
            - sms
        campaignStatus:
          type: string
          enum:
            - draft
            - scheduled
            - processing
            - paused
            - completed
            - failed
            - cancelled
        segment:
          $ref: "#/components/schemas/Segment"
        messageConfig:
          type: object
          additionalProperties: true
        stats:
          $ref: "#/components/schemas/CampaignStats"
        scheduledAt:
          type:
            - string
            - "null"
          format: date-time
        startedAt:
          type:
            - string
            - "null"
          format: date-time
        completedAt:
          type:
            - string
            - "null"
          format: date-time
    Comment:
      type: object
      properties:
        documentId:
          type: string
        content:
          type: string
          example: Cliente confirmou reunião para sexta.
        contentFormat:
          type: string
          enum:
            - plain
            - html
        type:
          type: string
          enum:
            - user
            - system
          description: Comentários `system` registram atividades automáticas do card.
        activityType:
          type:
            - string
            - "null"
          description: Preenchido em comentários `system` (ex. `phase_changed`, `assignee_added`).
        mentions:
          type: array
          items:
            type: string
          description: documentIds dos memberships mencionados.
        metadata:
          type: object
          description: Metadados (ex. `links` detectados no conteúdo).
        author:
          $ref: "#/components/schemas/UserRef"
        card:
          type: object
          properties:
            documentId:
              type: string
        createdAt:
          type: string
          format: date-time
    Team:
      type: object
      properties:
        documentId:
          type: string
        name:
          type: string
          example: Time SP
        slug:
          type: string
        description:
          type:
            - string
            - "null"
        color:
          type: string
          example: "#3B82F6"
        active:
          type: boolean
        leader:
          $ref: "#/components/schemas/MembershipRef"
        members:
          type: array
          items:
            $ref: "#/components/schemas/MembershipRef"
    QuoteItemInput:
      type: object
      description: Item de orçamento. `subtotal` e `total` são calculados no servidor.
      required:
        - name
      properties:
        product:
          type: string
          description: documentId do produto (opcional — itens livres são permitidos).
        name:
          type: string
          example: Consultoria estratégica
        sku:
          type: string
        quantity:
          type: integer
          minimum: 1
          default: 1
        periods:
          type: integer
          default: 1
          description: Nº de períodos (para produtos recorrentes).
        unitPrice:
          type: number
        discountPercent:
          type: number
          default: 0
        notes:
          type: string
    Quote:
      type: object
      properties:
        documentId:
          type: string
          example: qt1a2b3c
        number:
          type: string
          description: Gerado no servidor (formato `YYYY-00001`).
          example: 2026-00042
        quoteStatus:
          type: string
          enum:
            - draft
            - active
            - won
            - lost
        documentType:
          type: string
          enum:
            - quote
            - contract
            - report
            - other
        items:
          type: array
          items:
            allOf:
              - $ref: "#/components/schemas/QuoteItemInput"
              - type: object
                properties:
                  subtotal:
                    type: number
                  total:
                    type: number
        subtotal:
          type: number
          example: 26880
        discountType:
          type: string
          enum:
            - percentage
            - fixed
        discountValue:
          type: number
        discountTotal:
          type: number
        total:
          type: number
          example: 24192
        currency:
          type: string
          example: BRL
        validUntil:
          type:
            - string
            - "null"
          format: date-time
        notes:
          type:
            - string
            - "null"
        version:
          type: integer
        card:
          type: object
          properties:
            documentId:
              type: string
            title:
              type: string
        contact:
          $ref: "#/components/schemas/ContactRef"
        createdAt:
          type: string
          format: date-time
    Product:
      type: object
      properties:
        documentId:
          type: string
          example: pr9x8y7z
        name:
          type: string
          example: Plano Pro
        slug:
          type: string
        sku:
          type: string
          example: PLANO-PRO
        description:
          type:
            - string
            - "null"
        shortDescription:
          type:
            - string
            - "null"
        productType:
          type: string
          enum:
            - physical
            - digital
            - service
        billingType:
          type: string
          enum:
            - one_time
            - recurring
        price:
          type: number
          example: 990
        salePrice:
          type:
            - number
            - "null"
        minPrice:
          type:
            - number
            - "null"
        currency:
          type: string
          example: BRL
        active:
          type: boolean
        categories:
          type: array
          items:
            $ref: "#/components/schemas/ProductCategory"
    ProductCategory:
      type: object
      properties:
        documentId:
          type: string
        name:
          type: string
          example: Serviços
        slug:
          type: string
        description:
          type:
            - string
            - "null"
        color:
          type: string
          example: "#3b82f6"
        parent:
          type:
            - object
            - "null"
          properties:
            documentId:
              type: string
            name:
              type: string
    PdfTemplate:
      type: object
      properties:
        documentId:
          type: string
        name:
          type: string
          example: Proposta padrão
        slug:
          type: string
        description:
          type:
            - string
            - "null"
        templateType:
          type: string
          enum:
            - quote
            - invoice
            - contract
            - report
        layout:
          type: object
          description: Estrutura de página com blocos de cabeçalho, corpo e rodapé.
        styles:
          type: object
          description: Cores e tipografia do documento.
  responses:
    BadRequest:
      description: Requisição inválida (validação, header `x-tenant-id` ausente, payload malformado).
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
    Unauthorized:
      description: Token ausente, inválido ou expirado.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
          example:
            data: null
            error:
              status: 401
              name: UnauthorizedError
              message: Autenticação obrigatória
    Forbidden:
      description: Sem permissão para esta ação (papel/board) ou organização inativa.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
    NotFound:
      description: Recurso não encontrado nesta organização.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
paths:
  /auth/local:
    post:
      operationId: login
      tags:
        - Autenticação
      summary: Login (JWT de sessão)
      security: []
      description: |
        Autentica com e-mail e senha e retorna um **JWT de sessão** de curta duração.

        Para integrações, prefira um **token de API de longa duração**: gere pela
        interface (Configurações → Organização → aba API) ou via
        [`POST /users/generate-token`](#tag/autenticacao/post/users/generate-token).

        > Este endpoint **não** exige `x-tenant-id`.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - identifier
                - password
              properties:
                identifier:
                  type: string
                  description: E-mail do usuário.
                  example: voce@empresa.com
                password:
                  type: string
                  format: password
                  example: SuaSenha123!
      responses:
        "200":
          description: Autenticado com sucesso.
          content:
            application/json:
              schema:
                type: object
                properties:
                  jwt:
                    type: string
                    description: JWT de sessão (expira em poucas horas).
                  user:
                    type: object
                    properties:
                      id:
                        type: integer
                      username:
                        type: string
                      email:
                        type: string
              example:
                jwt: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
                user:
                  id: 42
                  username: luis
                  email: voce@empresa.com
        "400":
          description: Credenciais inválidas.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
      x-codeSamples:
        - lang: cURL
          label: cURL
          source: |
            curl -X POST "https://api.perpetuus.com.br/api/auth/local" \
              -H "Content-Type: application/json" \
              -d '{"identifier": "voce@empresa.com", "password": "SuaSenha123!"}'
  /users/generate-token:
    post:
      operationId: generateToken
      tags:
        - Autenticação
      summary: Gerar token de API
      description: |
        Gera um **token de API de longa duração** para integrações (BI, Zapier, Make,
        scripts). Requer um JWT de sessão válido no header `Authorization`.

        Expirações aceitas: `1d`, `30d`, `365d` e `36500d` (≈ 100 anos).

        > Este endpoint **não** exige `x-tenant-id` e tem **rate limit** reforçado.

        **Guarde o token com segurança** — trate-o como uma senha e use variáveis de
        ambiente. Ele não pode ser recuperado depois.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - expiration
              properties:
                expiration:
                  type: string
                  enum:
                    - 1d
                    - 30d
                    - 365d
                    - 36500d
                  example: 365d
      responses:
        "200":
          description: Token gerado.
          content:
            application/json:
              schema:
                type: object
                properties:
                  token:
                    type: string
                    description: "Token de API (JWT). Use como `Authorization: Bearer <token>`."
                  expiration:
                    type: string
                    example: 365d
        "400":
          description: "Expiração inválida (`Invalid expiration time. Allowed: 1d, 30d, 365d, 36500d`)."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          $ref: "#/components/responses/Unauthorized"
      x-codeSamples:
        - lang: cURL
          label: cURL
          source: |
            curl -X POST "https://api.perpetuus.com.br/api/users/generate-token" \
              -H "Authorization: Bearer SEU_JWT_DO_LOGIN" \
              -H "Content-Type: application/json" \
              -d '{"expiration": "365d"}'
  /boards:
    parameters:
      - $ref: "#/components/parameters/TenantHeader"
    get:
      operationId: listBoards
      tags:
        - Boards
      summary: Listar boards
      description: |
        Lista os boards da organização. Usuários que não são **ADMIN** veem apenas
        os boards dos quais são membros.

        A resposta inclui, por padrão, `phases` (com `fields` e `allowedTransitions`),
        `tags`, `owner`, `members` e `editors`.
      parameters:
        - $ref: "#/components/parameters/PaginationPage"
        - $ref: "#/components/parameters/PaginationPageSize"
        - $ref: "#/components/parameters/Sort"
        - $ref: "#/components/parameters/Populate"
      responses:
        "200":
          description: Lista paginada de boards.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/Board"
                  meta:
                    type: object
                    properties:
                      pagination:
                        $ref: "#/components/schemas/Pagination"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
      x-codeSamples:
        - lang: cURL
          label: cURL
          source: |
            curl "https://api.perpetuus.com.br/api/boards" \
              -H "Authorization: Bearer SEU_TOKEN" \
              -H "x-tenant-id: acme"
    post:
      operationId: createBoard
      tags:
        - Boards
      summary: Criar board
      description: |
        Cria um novo board. Sujeito ao limite de boards do seu plano.
        Use `template` para partir de um modelo pronto (padrão: `minimal-workflow`).
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - data
              properties:
                data:
                  type: object
                  required:
                    - name
                  properties:
                    name:
                      type: string
                      example: Funil de Vendas
                    boardType:
                      type: string
                      enum:
                        - kanban
                        - chat
                        - hybrid
                    icon:
                      type: string
                      example: layout-grid
                    color:
                      type: string
                    template:
                      type: string
                      example: minimal-workflow
                    boardSettings:
                      $ref: "#/components/schemas/BoardSettings"
      responses:
        "200":
          description: Board criado (objeto flat, sem envelope).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Board"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
  /boards/{id}:
    parameters:
      - $ref: "#/components/parameters/TenantHeader"
      - name: id
        in: path
        required: true
        description: documentId do board.
        schema:
          type: string
    get:
      operationId: getBoard
      tags:
        - Boards
      summary: Buscar board
      description: |
        Retorna um board pelo **documentId**, com fases, campos, tags e membros.

        💡 Use este endpoint para descobrir os `documentId` das fases (para mover
        cards) e os `slug` dos campos (para preencher `fields`).
      responses:
        "200":
          description: O board (objeto flat, sem envelope).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Board"
              example:
                documentId: bd1a2b3c
                name: Funil de Vendas
                slug: funil-de-vendas
                active: true
                boardType: kanban
                boardSettings:
                  showValue: true
                  currency: BRL
                  wonPhase: ph_won
                  lostPhase: null
                phases:
                  - documentId: ph0aaa
                    name: Formulário
                    order: 0
                    isPublic: true
                    fields:
                      - documentId: fl1bbb
                        label: Nome
                        slug: name
                        type: text
                        required: true
                  - documentId: ph1ccc
                    name: Novo lead
                    order: 1
                    maxTimeInPhaseHours: 48
                tags:
                  - documentId: tg1ddd
                    name: VIP
                    slug: vip
                    color: "#3b82f6"
        "404":
          $ref: "#/components/responses/NotFound"
    put:
      operationId: updateBoard
      tags:
        - Boards
      summary: Atualizar board
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                data:
                  type: object
                  description: Campos a alterar (parciais).
                  properties:
                    name:
                      type: string
                    active:
                      type: boolean
                    icon:
                      type: string
                    color:
                      type: string
                    boardSettings:
                      $ref: "#/components/schemas/BoardSettings"
      responses:
        "200":
          description: Board atualizado (objeto flat).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Board"
        "404":
          $ref: "#/components/responses/NotFound"
    delete:
      operationId: deleteBoard
      tags:
        - Boards
      summary: Excluir board
      description: ⚠️ Ação destrutiva — remove o board e seus cards.
      responses:
        "200":
          description: Board excluído.
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
  /boards/{id}/members:
    parameters:
      - $ref: "#/components/parameters/TenantHeader"
      - name: id
        in: path
        required: true
        description: documentId do board.
        schema:
          type: string
    get:
      operationId: listBoardMembers
      tags:
        - Membros do board
      summary: Listar membros
      description: Lista membros, editores e owner do board.
      responses:
        "200":
          description: Membros do board.
          content:
            application/json:
              schema:
                type: object
                properties:
                  members:
                    type: array
                    items:
                      $ref: "#/components/schemas/MembershipRef"
                  editors:
                    type: array
                    items:
                      $ref: "#/components/schemas/MembershipRef"
                  owner:
                    $ref: "#/components/schemas/MembershipRef"
        "404":
          $ref: "#/components/responses/NotFound"
    post:
      operationId: addBoardMember
      tags:
        - Membros do board
      summary: Adicionar membro
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - membershipId
              properties:
                membershipId:
                  type: string
                  description: documentId do membership da organização.
      responses:
        "200":
          description: Board atualizado, com `members` e `editors` populados.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Board"
        "400":
          description: Membership inválido ou usuário já é membro do board.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
  /boards/{id}/members/{membershipId}:
    parameters:
      - $ref: "#/components/parameters/TenantHeader"
      - name: id
        in: path
        required: true
        schema:
          type: string
      - name: membershipId
        in: path
        required: true
        schema:
          type: string
    delete:
      operationId: removeBoardMember
      tags:
        - Membros do board
      summary: Remover membro
      description: Remove o membership de `members` **e** `editors`. O owner do board não pode ser removido.
      responses:
        "200":
          description: Board atualizado.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Board"
        "400":
          description: Membership inválido ou tentativa de remover o owner.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
  /boards/{id}/members/bulk:
    parameters:
      - $ref: "#/components/parameters/TenantHeader"
      - name: id
        in: path
        required: true
        schema:
          type: string
    put:
      operationId: bulkUpdateBoardMembers
      tags:
        - Membros do board
      summary: Substituir membros em lote
      description: |
        Define de uma vez as listas completas de membros e editores do board.

        > ⚠️ Este endpoint usa **IDs numéricos** dos memberships (exceção à regra do
        > documentId). O owner é sempre mantido; editores precisam estar em `members`.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                members:
                  type: array
                  items:
                    type: integer
                  description: IDs numéricos dos memberships.
                editors:
                  type: array
                  items:
                    type: integer
                  description: IDs numéricos (subconjunto de `members`).
      responses:
        "200":
          description: Board atualizado.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Board"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
  /boards/{id}/editors:
    parameters:
      - $ref: "#/components/parameters/TenantHeader"
      - name: id
        in: path
        required: true
        schema:
          type: string
    post:
      operationId: addBoardEditor
      tags:
        - Membros do board
      summary: Adicionar editor
      description: Editores podem alterar a estrutura do board (fases, campos, tags).
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - membershipId
              properties:
                membershipId:
                  type: string
      responses:
        "200":
          description: Editor adicionado.
        "403":
          $ref: "#/components/responses/Forbidden"
  /boards/{id}/editors/{membershipId}:
    parameters:
      - $ref: "#/components/parameters/TenantHeader"
      - name: id
        in: path
        required: true
        schema:
          type: string
      - name: membershipId
        in: path
        required: true
        schema:
          type: string
    delete:
      operationId: removeBoardEditor
      tags:
        - Membros do board
      summary: Remover editor
      responses:
        "200":
          description: Editor removido.
        "403":
          $ref: "#/components/responses/Forbidden"
  /boards/{boardId}/report:
    parameters:
      - $ref: "#/components/parameters/TenantHeader"
      - name: boardId
        in: path
        required: true
        description: documentId do board.
        schema:
          type: string
    get:
      operationId: getBoardReport
      tags:
        - Relatórios
      summary: Relatório do board
      description: |
        Métricas agregadas do funil: totais, ganhos/perdas, breakdown por fase,
        responsável e tag, e criação por dia.

        - Sem `from`/`to`, o período padrão é **os últimos 15 dias**.
        - Valores monetários só são preenchidos quando `boardSettings.showValue = true`.
        - Ganho/perda derivam de `boardSettings.wonPhase` / `lostPhase`.

        **BI/ETL:** use este endpoint para dashboards agregados; para dados linha a
        linha, use `GET /boards/{boardId}/cards`.
      parameters:
        - name: from
          in: query
          description: Início do período (ISO 8601).
          schema:
            type: string
            format: date
            example: 2026-06-01
        - name: to
          in: query
          description: Fim do período (ISO 8601).
          schema:
            type: string
            format: date
            example: 2026-06-30
        - name: assignee
          in: query
          description: Filtrar por responsável (documentId do membership).
          schema:
            type: string
        - name: tag
          in: query
          description: Filtrar por tag (documentId).
          schema:
            type: string
      responses:
        "200":
          description: Relatório (objeto flat — **não** usa envelope `{ data, meta }`).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/BoardReport"
        "404":
          $ref: "#/components/responses/NotFound"
      x-codeSamples:
        - lang: cURL
          label: cURL
          source: |
            curl -G "https://api.perpetuus.com.br/api/boards/BOARD_ID/report" \
              -H "Authorization: Bearer SEU_TOKEN" \
              -H "x-tenant-id: acme" \
              --data-urlencode "from=2026-06-01" \
              --data-urlencode "to=2026-06-30"
  /boards/{boardId}/public-form:
    parameters:
      - name: boardId
        in: path
        required: true
        description: documentId do board.
        schema:
          type: string
    get:
      operationId: getPublicForm
      tags:
        - Formulário público
      summary: Estrutura do formulário público
      security: []
      description: |
        Retorna a definição do formulário de entrada do board (Fase 0): título,
        campos, cores e textos. **Não requer autenticação** — ideal para renderizar
        formulários de captura no seu site.
      responses:
        "200":
          description: Definição do formulário (campos da Fase 0).
        "404":
          $ref: "#/components/responses/NotFound"
    post:
      operationId: submitPublicForm
      tags:
        - Formulário público
      summary: Enviar formulário público
      security: []
      description: |
        Cria um card a partir do formulário público da Fase 0. **Não requer
        autenticação.** Os campos enviados devem corresponder aos slugs do formulário.

        Campos de rastreamento opcionais (`fbclid`, `gclid`, UTMs) são aceitos e
        anexados ao card.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - fields
              properties:
                fields:
                  type: object
                  additionalProperties: true
                  example:
                    name: João Silva
                    email: joao@cliente.com
                    phone: "+5511999999999"
      responses:
        "200":
          description: Lead registrado.
        "400":
          $ref: "#/components/responses/BadRequest"
        "404":
          $ref: "#/components/responses/NotFound"
  /boards/{boardId}/public-upload:
    parameters:
      - name: boardId
        in: path
        required: true
        schema:
          type: string
    post:
      operationId: publicUpload
      tags:
        - Formulário público
      summary: Upload de arquivo no formulário público
      security: []
      description: Upload de anexos para campos do tipo `file` do formulário público.
      requestBody:
        content:
          multipart/form-data:
            schema:
              type: object
              properties:
                files:
                  type: string
                  format: binary
      responses:
        "200":
          description: Arquivo recebido.
        "400":
          $ref: "#/components/responses/BadRequest"
  /boards/{boardId}/cards:
    parameters:
      - $ref: "#/components/parameters/TenantHeader"
      - name: boardId
        in: path
        required: true
        description: documentId do board.
        schema:
          type: string
    get:
      operationId: listCards
      tags:
        - Cards
      summary: Listar cards
      description: |
        Lista os cards do board, com paginação, filtros, ordenação e estatísticas.

        A resposta inclui por padrão `currentPhase`, `assignees`, `tags`, `contact`,
        `fieldData` e o mapa achatado **`fields`** (`slug → valor`, já desembrulhado).

        ### Filtros comuns

        | Filtro | Exemplo |
        |--------|---------|
        | Fase atual | `filters[currentPhase][documentId]=PHASE_ID` |
        | Título contém | `filters[title][$containsi]=joão` |
        | Tag | `filters[tags][documentId][$in][0]=TAG_ID` |
        | Campo dinâmico | `filters[fieldData][field][slug]=email&filters[fieldData][value][val]=lead@cliente.com` |
        | Vencimento | `filters[dueDate][$lt]=2026-08-01` |

        ### Estatísticas (`meta.stats`)

        Presentes por padrão; desligue com `stats=false`. Para alinhar a contagem de
        atrasados ao fuso local, envie `statsTodayStart` (ISO da meia-noite local).

        > 💡 Board inexistente retorna lista **vazia** (não 404).
      parameters:
        - $ref: "#/components/parameters/PaginationPage"
        - $ref: "#/components/parameters/PaginationPageSize"
        - $ref: "#/components/parameters/Sort"
        - $ref: "#/components/parameters/Populate"
        - name: stats
          in: query
          description: "`false` desliga o cálculo de `meta.stats`."
          schema:
            type: boolean
            default: true
        - name: statsTodayStart
          in: query
          description: ISO da meia-noite local, para contagem de atrasados no seu fuso.
          schema:
            type: string
            format: date-time
      responses:
        "200":
          description: Lista paginada com estatísticas.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/Card"
                  meta:
                    type: object
                    properties:
                      pagination:
                        $ref: "#/components/schemas/Pagination"
                      stats:
                        $ref: "#/components/schemas/CardStats"
              example:
                data:
                  - documentId: cd1a2b3c
                    title: João Silva
                    value: 15000.5
                    dueDate: 2026-07-20T00:00:00.000Z
                    currentPhase:
                      documentId: ph1ccc
                      name: Novo lead
                    assignees:
                      - documentId: mb1
                        user:
                          username: luis
                    tags:
                      - documentId: tg1ddd
                        name: VIP
                        slug: vip
                        color: "#3b82f6"
                    contact:
                      documentId: ct1
                      name: João Silva
                      email: joao@cliente.com
                    fields:
                      name: João Silva
                      email: joao@cliente.com
                      valor_orcamento: 25000
                meta:
                  pagination:
                    page: 1
                    pageSize: 25
                    pageCount: 4
                    total: 87
                  stats:
                    overdueCount: 3
                    valueSum: 125000.5
                    phaseSlaBreachedCount: 0
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
      x-codeSamples:
        - lang: cURL
          label: cURL
          source: |
            curl -G "https://api.perpetuus.com.br/api/boards/BOARD_ID/cards" \
              -H "Authorization: Bearer SEU_TOKEN" \
              -H "x-tenant-id: acme" \
              --data-urlencode "pagination[page]=1" \
              --data-urlencode "pagination[pageSize]=50" \
              --data-urlencode "sort=updatedAt:desc"
        - lang: JavaScript
          label: JavaScript
          source: |
            const params = new URLSearchParams({
              'pagination[page]': '1',
              'pagination[pageSize]': '100',
              sort: 'createdAt:asc',
            });

            const res = await fetch(
              `https://api.perpetuus.com.br/api/boards/${boardId}/cards?${params}`,
              { headers: { Authorization: `Bearer ${token}`, 'x-tenant-id': 'acme' } }
            );
            const { data, meta } = await res.json();
    post:
      operationId: createCard
      tags:
        - Cards
      summary: Criar card
      description: |
        Cria um card no board. O objeto **`fields` é obrigatório** e deve conter ao
        menos os campos obrigatórios da Fase 0 (formulário de entrada) — em geral, `name`.

        O card nasce na primeira fase de trabalho (order 1) com `title = fields.name`.

        - `tags` aceita **slug ou documentId** — tags inexistentes retornam `400`.
        - `dueDate` ausente pode ser derivado do SLA da fase inicial.

        💡 Descubra os slugs dos campos com `GET /boards/{boardId}` (fases → fields).
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - data
              properties:
                data:
                  type: object
                  required:
                    - fields
                  properties:
                    fields:
                      type: object
                      description: Mapa `slug → valor`. Deve incluir os obrigatórios da Fase 0.
                      additionalProperties: true
                    assignees:
                      type: array
                      items:
                        type: string
                      description: documentIds de memberships.
                    tags:
                      type: array
                      items:
                        type: string
                      description: Slugs **ou** documentIds de tags do board.
                    contact:
                      type: string
                      description: documentId de um contato.
                    dueDate:
                      type: string
                      format: date-time
                    description:
                      type: string
                      description: Texto rico (HTML).
                    value:
                      type: number
            example:
              data:
                fields:
                  name: Lead do formulário web
                  email: lead@cliente.com
                  phone: "+5511987654321"
                tags:
                  - novo-lead
                description: Originado via API
      responses:
        "200":
          description: Card criado (objeto flat, com `fields` e `fieldData`).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Card"
        "400":
          description: |
            Validação falhou. Possíveis mensagens:
            - `Campo "fields" é obrigatório e deve ser um objeto`
            - `Campo "name" é obrigatório`
            - `Validação de campos falhou` — com `details.fields: { slug: mensagem }`
            - Tags inexistentes — com a lista em `details.tags`
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          $ref: "#/components/responses/NotFound"
      x-codeSamples:
        - lang: cURL
          label: cURL
          source: |
            curl -X POST "https://api.perpetuus.com.br/api/boards/BOARD_ID/cards" \
              -H "Authorization: Bearer SEU_TOKEN" \
              -H "x-tenant-id: acme" \
              -H "Content-Type: application/json" \
              -d '{
                "data": {
                  "fields": {
                    "name": "Lead do formulário web",
                    "email": "lead@cliente.com",
                    "phone": "+5511987654321"
                  },
                  "description": "Originado via API"
                }
              }'
        - lang: JavaScript
          label: JavaScript
          source: |
            const res = await fetch(`https://api.perpetuus.com.br/api/boards/${boardId}/cards`, {
              method: 'POST',
              headers: {
                Authorization: `Bearer ${token}`,
                'x-tenant-id': 'acme',
                'Content-Type': 'application/json',
              },
              body: JSON.stringify({
                data: {
                  fields: { name: 'Nova oportunidade', email: 'cliente@email.com' },
                  tags: ['novo-lead'],
                },
              }),
            });

            if (!res.ok) {
              const err = await res.json();
              throw new Error(err.error?.message ?? 'Erro ao criar card');
            }

            const card = await res.json();
            console.log('Card criado:', card.documentId);
  /boards/{boardId}/cards/{id}:
    parameters:
      - $ref: "#/components/parameters/TenantHeader"
      - name: boardId
        in: path
        required: true
        description: documentId do board.
        schema:
          type: string
      - name: id
        in: path
        required: true
        description: documentId do card.
        schema:
          type: string
    get:
      operationId: getCard
      tags:
        - Cards
      summary: Buscar card
      description: Retorna um card com fase, responsáveis, tags, contato e campos dinâmicos.
      parameters:
        - $ref: "#/components/parameters/Populate"
      responses:
        "200":
          description: O card (objeto flat).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Card"
        "400":
          description: Card não pertence a este board.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          $ref: "#/components/responses/NotFound"
    put:
      operationId: updateCard
      tags:
        - Cards
      summary: Atualizar card (campos, fase, responsáveis)
      description: |
        Atualiza metadados, campos dinâmicos e/ou **move o card de fase** — envie
        apenas o que deseja alterar.

        ### Campos dinâmicos

        `fields` mapeia `slug → valor`; o backend descobre em qual fase cada campo
        está e faz o upsert. Os demais campos permanecem intactos.

        ### Mover de fase

        Envie `currentPhase` com o documentId da fase destino. Regras:
        - a fase precisa pertencer ao board (400 caso contrário);
        - não é possível mover para a Fase 0 (formulário);
        - ao avançar, os campos obrigatórios da fase de origem precisam estar
          preenchidos (`400 — Preencha os campos obrigatórios antes de avançar`).
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - data
              properties:
                data:
                  type: object
                  properties:
                    title:
                      type: string
                    description:
                      type: string
                    dueDate:
                      type: string
                      format: date-time
                    value:
                      type: number
                    assignees:
                      type: array
                      items:
                        type: string
                    tags:
                      type: array
                      items:
                        type: string
                      description: Slugs ou documentIds.
                    contact:
                      type: string
                    currentPhase:
                      type: string
                      description: documentId da fase destino (dispara a transição).
                    reason:
                      type: string
                      description: Motivo da transição (registrado no histórico).
                    fields:
                      type: object
                      additionalProperties: true
            examples:
              atualizar-campos:
                summary: Atualizar campos dinâmicos
                value:
                  data:
                    fields:
                      valor_orcamento: 35000
                      prioridade: URGENTE
              mover-fase:
                summary: Mover de fase (com campos da fase destino)
                value:
                  data:
                    currentPhase: fase-ganho-doc-id
                    fields:
                      valor_fechado: 50000
                      data_fechamento: 2026-07-10
              atualizar-metadados:
                summary: Atualizar título e valor
                value:
                  data:
                    title: Oportunidade — orçamento aprovado
                    value: 42000
      responses:
        "200":
          description: Card atualizado (objeto flat).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Card"
        "400":
          description: |
            Validação falhou — card de outro board, fase inválida, campos obrigatórios
            faltando (`details.errors`) ou tipos de campo inválidos (`details.fields`).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          $ref: "#/components/responses/NotFound"
      x-codeSamples:
        - lang: cURL
          label: cURL
          source: |
            curl -X PUT "https://api.perpetuus.com.br/api/boards/BOARD_ID/cards/CARD_ID" \
              -H "Authorization: Bearer SEU_TOKEN" \
              -H "x-tenant-id: acme" \
              -H "Content-Type: application/json" \
              -d '{
                "data": {
                  "currentPhase": "fase-ganho-doc-id",
                  "fields": { "valor_fechado": 50000 }
                }
              }'
    delete:
      operationId: deleteCard
      tags:
        - Cards
      summary: Excluir card
      responses:
        "200":
          description: Card excluído.
          content:
            application/json:
              schema:
                type: object
                properties:
                  message:
                    type: string
                    example: Card deletado com sucesso
        "400":
          description: Card não pertence a este board.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          $ref: "#/components/responses/NotFound"
  /boards/{boardId}/cards/linked:
    parameters:
      - $ref: "#/components/parameters/TenantHeader"
      - name: boardId
        in: path
        required: true
        schema:
          type: string
    post:
      operationId: createLinkedCard
      tags:
        - Cards vinculados
      summary: Criar card vinculado
      description: |
        Cria um card em **outro board** já vinculado ao card de origem, através de um
        campo do tipo `card_link` — útil para fluxos encadeados (ex.: venda ganha →
        card de onboarding).

        O campo (`sourceFieldId`) precisa ser do tipo `card_link` com board de destino
        configurado. Campos com automap copiam valores do card de origem; os
        obrigatórios da Fase 0 do board destino são validados.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - sourceCardId
                - sourceFieldId
                - linkedCardData
              properties:
                sourceCardId:
                  type: string
                  description: documentId do card de origem (deste board).
                sourceFieldId:
                  type: string
                  description: documentId do campo `card_link` que guarda o vínculo.
                linkedCardData:
                  type: object
                  properties:
                    title:
                      type: string
                    fields:
                      type: object
                      additionalProperties: true
      responses:
        "200":
          description: Card criado no board de destino e vinculado.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: "#/components/schemas/Card"
                  meta:
                    type: object
                    properties:
                      message:
                        type: string
                      link:
                        type: object
                        properties:
                          cardId:
                            type: string
                          boardId:
                            type: string
                          title:
                            type: string
                          createdAt:
                            type: string
                            format: date-time
        "400":
          description: Parâmetros faltando, campo não é `card_link`, board destino não configurado, cardinalidade 1:1 já preenchida ou validação de campos falhou.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
  /boards/{boardId}/cards/link-existing:
    parameters:
      - $ref: "#/components/parameters/TenantHeader"
      - name: boardId
        in: path
        required: true
        schema:
          type: string
    post:
      operationId: linkExistingCard
      tags:
        - Cards vinculados
      summary: Vincular card existente
      description: |
        Vincula um card já existente do board de destino ao campo `card_link` do card
        de origem. Operação **idempotente** — se o par já estiver vinculado, retorna
        `meta.added: false`.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - sourceCardId
                - sourceFieldId
                - targetCardId
              properties:
                sourceCardId:
                  type: string
                  description: documentId do card deste board.
                sourceFieldId:
                  type: string
                  description: documentId do campo `card_link`.
                targetCardId:
                  type: string
                  description: documentId do card do board de destino.
      responses:
        "200":
          description: Vínculo criado (ou já existente).
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      link:
                        type: object
                        properties:
                          cardId:
                            type: string
                          boardId:
                            type: string
                          title:
                            type: string
                          createdAt:
                            type: string
                            format: date-time
                  meta:
                    type: object
                    properties:
                      added:
                        type: boolean
        "400":
          $ref: "#/components/responses/BadRequest"
        "404":
          $ref: "#/components/responses/NotFound"
  /boards/{boardId}/cards/search-for-link:
    parameters:
      - $ref: "#/components/parameters/TenantHeader"
      - name: boardId
        in: path
        required: true
        schema:
          type: string
    get:
      operationId: searchCardsForLink
      tags:
        - Cards vinculados
      summary: Buscar cards para vincular
      description: Busca cards no board de destino configurado no campo `card_link`.
      parameters:
        - name: fieldId
          in: query
          required: true
          description: documentId do campo `card_link`.
          schema:
            type: string
        - name: q
          in: query
          description: Termo de busca (título do card, case-insensitive).
          schema:
            type: string
        - name: page
          in: query
          schema:
            type: integer
            default: 1
        - name: pageSize
          in: query
          schema:
            type: integer
            maximum: 50
        - name: excludeCardId
          in: query
          description: documentId de card a excluir dos resultados.
          schema:
            type: string
      responses:
        "200":
          description: Cards encontrados.
          content:
            application/json:
              schema:
                type: object
                properties:
                  results:
                    type: array
                    items:
                      type: object
                      properties:
                        documentId:
                          type: string
                        title:
                          type: string
                        currentPhase:
                          type:
                            - object
                            - "null"
                          properties:
                            documentId:
                              type: string
                            name:
                              type: string
                        createdAt:
                          type: string
                          format: date-time
                  pagination:
                    $ref: "#/components/schemas/Pagination"
        "400":
          description: "`fieldId` ausente ou campo em modo `create` (busca bloqueada)."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
  /boards/{boardId}/cards/{cardId}/links/{targetCardId}:
    parameters:
      - $ref: "#/components/parameters/TenantHeader"
      - name: boardId
        in: path
        required: true
        schema:
          type: string
      - name: cardId
        in: path
        required: true
        schema:
          type: string
      - name: targetCardId
        in: path
        required: true
        schema:
          type: string
    delete:
      operationId: unlinkCard
      tags:
        - Cards vinculados
      summary: Remover vínculo entre cards
      parameters:
        - name: fieldId
          in: query
          required: true
          description: documentId do campo `card_link` que guarda o vínculo.
          schema:
            type: string
      responses:
        "200":
          description: Vínculo removido.
          content:
            application/json:
              schema:
                type: object
                properties:
                  message:
                    type: string
                    example: Vínculo removido com sucesso
        "400":
          $ref: "#/components/responses/BadRequest"
        "404":
          $ref: "#/components/responses/NotFound"
  /boards/{boardId}/phases:
    parameters:
      - $ref: "#/components/parameters/TenantHeader"
      - name: boardId
        in: path
        required: true
        description: documentId do board.
        schema:
          type: string
    get:
      operationId: listPhases
      tags:
        - Fases
      summary: Listar fases
      description: |
        Lista as fases do board em ordem (`order:asc`), com seus campos.
        A fase de `order` 0 é o **formulário de entrada** — cards não ficam nela.
      parameters:
        - $ref: "#/components/parameters/Sort"
        - name: limit
          in: query
          schema:
            type: integer
        - name: offset
          in: query
          schema:
            type: integer
      responses:
        "200":
          description: Array de fases (flat, sem envelope).
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/Phase"
        "404":
          $ref: "#/components/responses/NotFound"
      x-codeSamples:
        - lang: cURL
          label: cURL
          source: |
            curl "https://api.perpetuus.com.br/api/boards/BOARD_ID/phases" \
              -H "Authorization: Bearer SEU_TOKEN" \
              -H "x-tenant-id: acme"
    post:
      operationId: createPhase
      tags:
        - Fases
      summary: Criar fase
      description: |
        Cria uma fase no board. Se `order` for omitido, a fase entra no fim;
        inserções no meio reordenam as demais automaticamente.

        - Fases de `order` 0 são sempre públicas (formulário).
        - `fields` opcional cria os campos junto (slugs duplicados retornam 400).
        - `maxTimeInPhaseHours` define o SLA da fase (1–8760 horas; `null` remove).
        - As transições permitidas são sincronizadas automaticamente na criação.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - data
              properties:
                data:
                  type: object
                  required:
                    - name
                  properties:
                    name:
                      type: string
                      example: Qualificação
                    slug:
                      type: string
                    order:
                      type: integer
                      minimum: 0
                    description:
                      type: string
                    maxTimeInPhaseHours:
                      type:
                        - integer
                        - "null"
                      minimum: 1
                      maximum: 8760
                    fields:
                      type: array
                      items:
                        $ref: "#/components/schemas/FieldDefinition"
      responses:
        "200":
          description: Fase criada (flat).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Phase"
        "400":
          description: Slugs de campo duplicados ou em conflito com campos existentes do board.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          $ref: "#/components/responses/NotFound"
  /boards/{boardId}/phases/{phaseId}:
    parameters:
      - $ref: "#/components/parameters/TenantHeader"
      - name: boardId
        in: path
        required: true
        schema:
          type: string
      - name: phaseId
        in: path
        required: true
        description: documentId da fase.
        schema:
          type: string
    get:
      operationId: getPhase
      tags:
        - Fases
      summary: Buscar fase
      responses:
        "200":
          description: A fase (flat).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Phase"
        "404":
          $ref: "#/components/responses/NotFound"
    put:
      operationId: updatePhase
      tags:
        - Fases
      summary: Atualizar fase
      description: Atualiza nome, ordem, SLA, transições permitidas e textos do formulário.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                data:
                  type: object
                  properties:
                    name:
                      type: string
                    order:
                      type: integer
                    description:
                      type: string
                    maxTimeInPhaseHours:
                      type:
                        - integer
                        - "null"
                    allowedTransitions:
                      type: array
                      items:
                        $ref: "#/components/schemas/AllowedTransition"
      responses:
        "200":
          description: Fase atualizada.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Phase"
        "404":
          $ref: "#/components/responses/NotFound"
    delete:
      operationId: deletePhase
      tags:
        - Fases
      summary: Excluir fase
      responses:
        "200":
          description: Fase excluída.
        "404":
          $ref: "#/components/responses/NotFound"
  /boards/{boardId}/fields:
    parameters:
      - $ref: "#/components/parameters/TenantHeader"
      - name: boardId
        in: path
        required: true
        schema:
          type: string
    get:
      operationId: listFields
      tags:
        - Campos
      summary: Listar campos do board
      description: |
        Lista os campos dinâmicos de **todas as fases** do board, com a fase de cada um.

        💡 Use este endpoint para descobrir os `slug` aceitos em `fields` ao criar ou
        atualizar cards.
      parameters:
        - $ref: "#/components/parameters/Sort"
      responses:
        "200":
          description: Array de campos (flat), ordenado por `order:asc`.
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/FieldDefinition"
        "404":
          $ref: "#/components/responses/NotFound"
    post:
      operationId: createField
      tags:
        - Campos
      summary: Criar campo
      description: |
        Cria um campo dinâmico. Todo campo pertence a uma **fase** — envie `phase`
        com o documentId da fase.

        Tipos disponíveis: `text, textarea, richtext, email, number, date, datetime,
        select, multiselect, radio, switch, file, url, phone, card_link, hidden,
        statement, time, currency, document, checkbox`.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - data
              properties:
                data:
                  type: object
                  required:
                    - label
                    - slug
                    - type
                    - phase
                  properties:
                    label:
                      type: string
                      example: Valor do orçamento
                    slug:
                      type: string
                      example: valor_orcamento
                    type:
                      type: string
                      example: currency
                    phase:
                      type: string
                      description: documentId da fase.
                    order:
                      type: integer
                    required:
                      type: boolean
                    placeholder:
                      type: string
                    helpText:
                      type: string
                    options:
                      type: array
                      items: {}
      responses:
        "200":
          description: Campo criado (flat).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/FieldDefinition"
        "400":
          $ref: "#/components/responses/BadRequest"
        "404":
          $ref: "#/components/responses/NotFound"
  /boards/{boardId}/fields/{fieldId}:
    parameters:
      - $ref: "#/components/parameters/TenantHeader"
      - name: boardId
        in: path
        required: true
        schema:
          type: string
      - name: fieldId
        in: path
        required: true
        description: documentId do campo.
        schema:
          type: string
    get:
      operationId: getField
      tags:
        - Campos
      summary: Buscar campo
      responses:
        "200":
          description: O campo (flat).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/FieldDefinition"
        "404":
          $ref: "#/components/responses/NotFound"
    put:
      operationId: updateField
      tags:
        - Campos
      summary: Atualizar campo
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                data:
                  type: object
                  properties:
                    label:
                      type: string
                    required:
                      type: boolean
                    order:
                      type: integer
                    placeholder:
                      type: string
                    helpText:
                      type: string
                    options:
                      type: array
                      items: {}
      responses:
        "200":
          description: Campo atualizado.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/FieldDefinition"
        "404":
          $ref: "#/components/responses/NotFound"
    delete:
      operationId: deleteField
      tags:
        - Campos
      summary: Excluir campo
      description: ⚠️ Os valores preenchidos deste campo nos cards deixam de ser exibidos.
      responses:
        "200":
          description: Campo excluído.
        "404":
          $ref: "#/components/responses/NotFound"
  /boards/{boardId}/field-conditions:
    parameters:
      - $ref: "#/components/parameters/TenantHeader"
      - name: boardId
        in: path
        required: true
        schema:
          type: string
    get:
      operationId: listFieldConditions
      tags:
        - Condições de campo
      summary: Listar condições
      description: Lista as regras de exibição condicional de campos do board, ordenadas por fase e ordem.
      parameters:
        - name: phaseId
          in: query
          description: Filtrar por fase.
          schema:
            type: string
      responses:
        "200":
          description: Array de condições (flat).
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/FieldCondition"
        "404":
          $ref: "#/components/responses/NotFound"
    post:
      operationId: createFieldCondition
      tags:
        - Condições de campo
      summary: Criar condição
      description: |
        Cria uma regra do tipo "quando *critérios*, então *mostrar/ocultar campos*".

        - `criteria`: grupos de expressões `{ fieldSlug, operator, value }` combinadas com `AND`.
        - `actionsWhenTrue` / `actionsWhenFalse`: ações `{ type: "show"|"hide", targetFieldSlugs: [...] }`.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - data
              properties:
                data:
                  type: object
                  required:
                    - name
                  properties:
                    name:
                      type: string
                      example: Mostrar CNPJ para empresas
                    scope:
                      type: string
                      enum:
                        - phase
                        - board
                      default: phase
                    phase:
                      type: string
                      description: documentId da fase (quando `scope = phase`).
                    criteria:
                      type: array
                      items:
                        type: object
                    actionsWhenTrue:
                      type: array
                      items:
                        type: object
                    actionsWhenFalse:
                      type: array
                      items:
                        type: object
                    enabled:
                      type: boolean
                      default: true
            example:
              data:
                name: Mostrar CNPJ para empresas
                scope: phase
                phase: ph1ccc
                criteria:
                  - operator: AND
                    expressions:
                      - fieldSlug: tipo_cliente
                        operator: eq
                        value: empresa
                actionsWhenTrue:
                  - type: show
                    targetFieldSlugs:
                      - cnpj
                      - razao_social
                actionsWhenFalse:
                  - type: hide
                    targetFieldSlugs:
                      - cnpj
                      - razao_social
      responses:
        "200":
          description: Condição criada (flat).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/FieldCondition"
        "400":
          $ref: "#/components/responses/BadRequest"
        "404":
          $ref: "#/components/responses/NotFound"
  /boards/{boardId}/field-conditions/{id}:
    parameters:
      - $ref: "#/components/parameters/TenantHeader"
      - name: boardId
        in: path
        required: true
        schema:
          type: string
      - name: id
        in: path
        required: true
        description: documentId da condição.
        schema:
          type: string
    put:
      operationId: updateFieldCondition
      tags:
        - Condições de campo
      summary: Atualizar condição
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                data:
                  type: object
      responses:
        "200":
          description: Condição atualizada.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/FieldCondition"
        "404":
          $ref: "#/components/responses/NotFound"
    delete:
      operationId: deleteFieldCondition
      tags:
        - Condições de campo
      summary: Excluir condição
      responses:
        "200":
          description: Condição excluída.
          content:
            application/json:
              schema:
                type: object
                properties:
                  message:
                    type: string
                    example: Condição deletada com sucesso
        "404":
          $ref: "#/components/responses/NotFound"
  /boards/{boardId}/field-conditions/reorder:
    parameters:
      - $ref: "#/components/parameters/TenantHeader"
      - name: boardId
        in: path
        required: true
        schema:
          type: string
    patch:
      operationId: reorderFieldConditions
      tags:
        - Condições de campo
      summary: Reordenar condições
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - items
              properties:
                items:
                  type: array
                  items:
                    type: object
                    required:
                      - documentId
                      - order
                    properties:
                      documentId:
                        type: string
                      order:
                        type: integer
      responses:
        "200":
          description: Lista completa reordenada (flat).
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/FieldCondition"
        "400":
          $ref: "#/components/responses/BadRequest"
  /boards/{boardId}/field-conditions/evaluate:
    parameters:
      - $ref: "#/components/parameters/TenantHeader"
      - name: boardId
        in: path
        required: true
        schema:
          type: string
    post:
      operationId: evaluateFieldConditions
      tags:
        - Condições de campo
      summary: Avaliar condições
      description: |
        Avalia as condições de uma fase contra um conjunto de valores e retorna quais
        campos ficam visíveis/ocultos — útil para renderizar formulários externos com
        a mesma lógica condicional do app.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - phaseId
              properties:
                phaseId:
                  type: string
                values:
                  type: object
                  additionalProperties: true
            example:
              phaseId: ph1ccc
              values:
                tipo_cliente: empresa
      responses:
        "200":
          description: Resultado da avaliação.
          content:
            application/json:
              schema:
                type: object
                properties:
                  phaseId:
                    type: string
                  visibleSlugs:
                    type: array
                    items:
                      type: string
                  hiddenSlugs:
                    type: array
                    items:
                      type: string
                  requiredOverrides:
                    type: object
                  evaluatedConditions:
                    type: integer
        "400":
          $ref: "#/components/responses/BadRequest"
        "404":
          $ref: "#/components/responses/NotFound"
  /boards/{boardId}/tags:
    parameters:
      - $ref: "#/components/parameters/TenantHeader"
      - name: boardId
        in: path
        required: true
        schema:
          type: string
    get:
      operationId: listTags
      tags:
        - Tags
      summary: Listar tags
      description: Lista as tags do board. Cada tag expõe `slug` e `documentId` — ambos aceitos ao taggear cards.
      responses:
        "200":
          description: Array de tags (flat).
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/Tag"
        "404":
          $ref: "#/components/responses/NotFound"
    post:
      operationId: createTag
      tags:
        - Tags
      summary: Criar tag
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - data
              properties:
                data:
                  type: object
                  required:
                    - name
                  properties:
                    name:
                      type: string
                      maxLength: 100
                      example: VIP
                    slug:
                      type: string
                    color:
                      type: string
                      description: Hex `#RGB` ou `#RRGGBB`.
                      default: "#3b82f6"
      responses:
        "200":
          description: Tag criada.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Tag"
        "400":
          $ref: "#/components/responses/BadRequest"
        "404":
          $ref: "#/components/responses/NotFound"
  /boards/{boardId}/tags/{tagId}:
    parameters:
      - $ref: "#/components/parameters/TenantHeader"
      - name: boardId
        in: path
        required: true
        schema:
          type: string
      - name: tagId
        in: path
        required: true
        description: documentId da tag.
        schema:
          type: string
    put:
      operationId: updateTag
      tags:
        - Tags
      summary: Atualizar tag
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                data:
                  type: object
                  properties:
                    name:
                      type: string
                    color:
                      type: string
      responses:
        "200":
          description: Tag atualizada.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Tag"
        "404":
          $ref: "#/components/responses/NotFound"
    delete:
      operationId: deleteTag
      tags:
        - Tags
      summary: Excluir tag
      responses:
        "200":
          description: Tag excluída (removida de todos os cards).
        "404":
          $ref: "#/components/responses/NotFound"
  /contacts:
    parameters:
      - $ref: "#/components/parameters/TenantHeader"
    get:
      operationId: listContacts
      tags:
        - Contatos
      summary: Listar contatos
      description: |
        Lista os contatos da organização, com paginação e filtros.

        - `quickSearch` busca em nome, e-mail, telefone, CPF e CNPJ.
        - `segment` filtra pelas regras de um segmento (JSON ou base64url).
        - `quickSearch` e `segment` **não podem ser combinados** (400).

        Cada contato retorna `customFields` como objeto `chave → valor`.
      parameters:
        - $ref: "#/components/parameters/PaginationPage"
        - $ref: "#/components/parameters/PaginationPageSize"
        - $ref: "#/components/parameters/Sort"
        - name: quickSearch
          in: query
          description: Busca rápida em nome/e-mail/telefone/CPF/CNPJ.
          schema:
            type: string
        - name: segment
          in: query
          description: Regras de segmento (JSON, objeto querystring ou base64url).
          schema:
            type: string
      responses:
        "200":
          description: Lista paginada de contatos.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/Contact"
                  meta:
                    type: object
                    properties:
                      pagination:
                        $ref: "#/components/schemas/Pagination"
        "400":
          $ref: "#/components/responses/BadRequest"
      x-codeSamples:
        - lang: cURL
          label: cURL
          source: |
            curl -G "https://api.perpetuus.com.br/api/contacts" \
              -H "Authorization: Bearer SEU_TOKEN" \
              -H "x-tenant-id: acme" \
              --data-urlencode "quickSearch=joao" \
              --data-urlencode "pagination[pageSize]=50"
    post:
      operationId: createContact
      tags:
        - Contatos
      summary: Criar contato
      description: |
        Cria um contato. Regras:
        - `name` é obrigatório; e-mail **ou** telefone é necessário.
        - E-mail, telefone, CPF e CNPJ são **únicos por organização** (400 se duplicado).
        - CPF/CNPJ são validados algoritmicamente e armazenados sem máscara.
        - `customFields` grava campos customizados definidos na organização.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - data
              properties:
                data:
                  type: object
                  required:
                    - name
                  properties:
                    name:
                      type: string
                    email:
                      type: string
                    phone:
                      type: string
                    contact_type:
                      type: string
                      enum:
                        - pessoa
                        - empresa
                    cpf:
                      type: string
                    cnpj:
                      type: string
                    address:
                      type: string
                    city:
                      type: string
                    state:
                      type: string
                    country:
                      type: string
                    postal_code:
                      type: string
                    birthdate:
                      type: string
                      format: date
                    first_buy_date:
                      type: string
                      format: date
                    owner:
                      type: string
                      description: documentId do membership responsável (define também `team` e `ownedAt`).
                    customFields:
                      type: object
                      additionalProperties: true
            example:
              data:
                name: João Silva
                email: joao@cliente.com
                phone: "+5511999999999"
                contact_type: pessoa
                city: São Paulo
                customFields:
                  origem: Indicação
      responses:
        "200":
          description: Contato criado (objeto flat, com `customFields`).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Contact"
        "400":
          description: Validação — nome ausente, e-mail/telefone/CPF/CNPJ duplicados ou inválidos.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
  /contacts/{id}:
    parameters:
      - $ref: "#/components/parameters/TenantHeader"
      - name: id
        in: path
        required: true
        description: documentId do contato.
        schema:
          type: string
    get:
      operationId: getContact
      tags:
        - Contatos
      summary: Buscar contato
      responses:
        "200":
          description: O contato (objeto flat, com `customFields`).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Contact"
        "404":
          $ref: "#/components/responses/NotFound"
    put:
      operationId: updateContact
      tags:
        - Contatos
      summary: Atualizar contato
      description: |
        Atualização parcial — envie apenas o que mudou. Não é possível deixar e-mail
        **e** telefone vazios ao mesmo tempo (400).
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - data
              properties:
                data:
                  type: object
                  properties:
                    name:
                      type: string
                    email:
                      type: string
                    phone:
                      type: string
                    customFields:
                      type: object
                      additionalProperties: true
      responses:
        "200":
          description: Contato atualizado (flat).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Contact"
        "400":
          $ref: "#/components/responses/BadRequest"
        "404":
          $ref: "#/components/responses/NotFound"
    delete:
      operationId: deleteContact
      tags:
        - Contatos
      summary: Excluir contato
      description: |
        Contatos com cards vinculados **não podem ser excluídos** — a API retorna 400
        com o código `CONTACT_HAS_LINKED_CARDS` e `linkedCardsCount` nos detalhes.
      responses:
        "200":
          description: Contato excluído.
        "400":
          description: Contato possui cards vinculados (`CONTACT_HAS_LINKED_CARDS`).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          $ref: "#/components/responses/NotFound"
  /contacts/{id}/linked-cards:
    parameters:
      - $ref: "#/components/parameters/TenantHeader"
      - name: id
        in: path
        required: true
        schema:
          type: string
    get:
      operationId: getContactLinkedCards
      tags:
        - Contatos
      summary: Cards vinculados ao contato
      responses:
        "200":
          description: Cards do contato (resumidos).
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      type: object
        "404":
          $ref: "#/components/responses/NotFound"
  /contacts/{id}/transfer-owner:
    parameters:
      - $ref: "#/components/parameters/TenantHeader"
      - name: id
        in: path
        required: true
        schema:
          type: string
    put:
      operationId: transferContactOwner
      tags:
        - Contatos
      summary: Transferir responsável
      description: |
        Transfere a carteira do contato para outro membership. Atualiza `owner`,
        `team` (herdado do novo responsável) e `ownedAt`, e registra a atividade
        `owner_changed` na timeline.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - newOwnerId
              properties:
                newOwnerId:
                  type: string
                  description: documentId do membership do novo responsável.
      responses:
        "200":
          description: Contato atualizado (flat).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Contact"
        "400":
          $ref: "#/components/responses/BadRequest"
        "404":
          $ref: "#/components/responses/NotFound"
  /contacts/bulk:
    parameters:
      - $ref: "#/components/parameters/TenantHeader"
    post:
      operationId: bulkImportContacts
      tags:
        - Contatos
      summary: Importar contatos em massa
      description: |
        Importa contatos em lote. Cada item precisa de `name` e de um e-mail **ou**
        telefone válido (telefones são normalizados para E.164; linhas inválidas são
        ignoradas e reportadas em `errors`).

        Campos customizados podem ser enviados diretamente por slug em cada item.

        A resposta inclui `bulkImportJobDocumentId` para acompanhar o processamento
        via `GET /contacts/bulk-import-status/{documentId}`.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - items
              properties:
                items:
                  type: array
                  items:
                    type: object
                    required:
                      - name
                    additionalProperties: true
                    properties:
                      name:
                        type: string
                      email:
                        type: string
                      phone:
                        type: string
                options:
                  type: object
                  properties:
                    triggerAutomations:
                      type: boolean
                    autoAssignOwner:
                      type: boolean
            example:
              items:
                - name: João Silva
                  email: joao@cliente.com
                  phone: "+5511999999999"
                  origem: Planilha
                - name: Maria Souza
                  email: maria@cliente.com
              options:
                autoAssignOwner: true
      responses:
        "200":
          description: Resultado da importação.
          content:
            application/json:
              schema:
                type: object
                properties:
                  message:
                    type: string
                  results:
                    type: object
                    properties:
                      success:
                        type: integer
                      failed:
                        type: integer
                      total:
                        type: integer
                      errors:
                        type: array
                        items:
                          type: string
                  bulkImportJobDocumentId:
                    type: string
        "400":
          $ref: "#/components/responses/BadRequest"
  /contacts/bulk-import-status/{documentId}:
    parameters:
      - $ref: "#/components/parameters/TenantHeader"
      - name: documentId
        in: path
        required: true
        description: documentId do job de importação.
        schema:
          type: string
    get:
      operationId: getBulkImportStatus
      tags:
        - Contatos
      summary: Status da importação em massa
      responses:
        "200":
          description: Status do job.
          content:
            application/json:
              schema:
                type: object
                properties:
                  documentId:
                    type: string
                  status:
                    type: string
                  contactsImported:
                    type: integer
                  finishedAt:
                    type:
                      - string
                      - "null"
                    format: date-time
        "404":
          $ref: "#/components/responses/NotFound"
  /contacts/bulk-delete:
    parameters:
      - $ref: "#/components/parameters/TenantHeader"
    post:
      operationId: bulkDeleteContacts
      tags:
        - Contatos
      summary: Excluir contatos em massa
      description: |
        Exclui vários contatos de uma vez. Contatos com cards vinculados são
        reportados em `failures` com o código `CONTACT_HAS_LINKED_CARDS`.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - documentIds
              properties:
                documentIds:
                  type: array
                  items:
                    type: string
      responses:
        "200":
          description: Resultado da exclusão em massa.
          content:
            application/json:
              schema:
                type: object
                properties:
                  deleted:
                    type: integer
                  failed:
                    type: integer
                  notFound:
                    type: integer
                  failures:
                    type: array
                    items:
                      type: object
                      properties:
                        documentId:
                          type: string
                        code:
                          type: string
                        message:
                          type: string
                        linkedCardsCount:
                          type: integer
        "400":
          $ref: "#/components/responses/BadRequest"
  /contacts/bulk-update:
    parameters:
      - $ref: "#/components/parameters/TenantHeader"
    post:
      operationId: bulkUpdateContacts
      tags:
        - Contatos
      summary: Atualizar contatos em massa
      description: |
        Aplica um `patch` a um conjunto de contatos definido por `scope`:

        | `scope.type` | Alvos |
        |--------------|-------|
        | `selected` | `scope.documentIds` |
        | `filter` | `scope.rules` (regras de filtro) |
        | `segment_filters` | `scope.filters.conditions` (regras de segmento) |
        | `quick_search` | `scope.q` (busca rápida) |

        O `patch` aceita `newOwnerDocumentId`, `fields` (campos nativos permitidos) e
        `customFields` — pelo menos um é obrigatório.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - scope
                - patch
              properties:
                scope:
                  type: object
                  required:
                    - type
                  properties:
                    type:
                      type: string
                      enum:
                        - selected
                        - filter
                        - segment_filters
                        - quick_search
                    documentIds:
                      type: array
                      items:
                        type: string
                    rules:
                      type: object
                    filters:
                      $ref: "#/components/schemas/SegmentFilters"
                    q:
                      type: string
                patch:
                  type: object
                  properties:
                    newOwnerDocumentId:
                      type: string
                    fields:
                      type: object
                      additionalProperties: true
                    customFields:
                      type: object
                      additionalProperties: true
            example:
              scope:
                type: selected
                documentIds:
                  - ct1a2b3c
                  - ct4d5e6f
              patch:
                newOwnerDocumentId: mb9z8y7x
      responses:
        "200":
          description: Resultado da atualização em massa.
          content:
            application/json:
              schema:
                type: object
                properties:
                  updated:
                    type: integer
                  skippedOwner:
                    type: integer
                  failed:
                    type: integer
                  totalTargets:
                    type: integer
                  failures:
                    type: array
                    items:
                      type: object
        "400":
          $ref: "#/components/responses/BadRequest"
  /contacts/export:
    parameters:
      - $ref: "#/components/parameters/TenantHeader"
    post:
      operationId: exportContacts
      tags:
        - Contatos
      summary: Exportar contatos (CSV)
      description: |
        Exporta contatos em **CSV** (`text/csv`). Aceita os mesmos filtros da
        listagem (`filters`, `segment`, `quickSearch`, `sort`) — `segment` e
        `quickSearch` não podem ser combinados.

        O total exportado vem no header `X-Export-Total`. Colunas incluem dados do
        contato, responsável, time e campos customizados.
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                filters:
                  type: object
                segment:
                  type: object
                quickSearch:
                  type: string
                sort:
                  type: string
      responses:
        "200":
          description: Arquivo CSV.
          content:
            text/csv:
              schema:
                type: string
        "400":
          $ref: "#/components/responses/BadRequest"
  /contact-activities:
    parameters:
      - $ref: "#/components/parameters/TenantHeader"
    get:
      operationId: listContactActivities
      tags:
        - Atividades de contato
      summary: Listar atividades
      description: |
        Timeline de eventos dos contatos, ordenada por `performedAt:desc`.
        Filtre por contato com `contactId` (ou `filters[contact][documentId]`).
      parameters:
        - name: contactId
          in: query
          description: documentId do contato.
          schema:
            type: string
      responses:
        "200":
          description: Atividades.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/ContactActivity"
        "400":
          $ref: "#/components/responses/BadRequest"
  /contact-activities/{id}:
    parameters:
      - $ref: "#/components/parameters/TenantHeader"
      - name: id
        in: path
        required: true
        schema:
          type: string
    get:
      operationId: getContactActivity
      tags:
        - Atividades de contato
      summary: Buscar atividade
      responses:
        "200":
          description: A atividade (objeto flat), com `contact` e `actor` populados.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ContactActivity"
        "404":
          $ref: "#/components/responses/NotFound"
  /contact-activities/note:
    parameters:
      - $ref: "#/components/parameters/TenantHeader"
    post:
      operationId: createContactNote
      tags:
        - Atividades de contato
      summary: Adicionar nota ao contato
      description: |
        Registra uma nota (`note_added`) na timeline do contato. A nota é atribuída
        ao usuário autenticado e limitada a 5.000 caracteres.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - contactId
                - note
              properties:
                contactId:
                  type: string
                  description: documentId do contato.
                note:
                  type: string
                  maxLength: 5000
            example:
              contactId: ct1a2b3c
              note: Cliente pediu retorno na sexta-feira.
      responses:
        "200":
          description: Nota criada.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: "#/components/schemas/ContactActivity"
        "400":
          description: "`contactId` ou `note` ausentes."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          $ref: "#/components/responses/NotFound"
      x-codeSamples:
        - lang: cURL
          label: cURL
          source: |
            curl -X POST "https://api.perpetuus.com.br/api/contact-activities/note" \
              -H "Authorization: Bearer SEU_TOKEN" \
              -H "x-tenant-id: acme" \
              -H "Content-Type: application/json" \
              -d '{"contactId": "ct1a2b3c", "note": "Cliente pediu retorno na sexta."}'
  /segments:
    parameters:
      - $ref: "#/components/parameters/TenantHeader"
    get:
      operationId: listSegments
      tags:
        - Segmentos
      summary: Listar segmentos
      description: |
        Lista os segmentos da organização. Com `pagination[page]`/`[pageSize]` retorna
        `{ data, meta.pagination }`; sem paginação, retorna `{ data }` com a lista completa.
      parameters:
        - name: search
          in: query
          description: Busca em nome e descrição.
          schema:
            type: string
        - $ref: "#/components/parameters/PaginationPage"
        - $ref: "#/components/parameters/PaginationPageSize"
        - $ref: "#/components/parameters/Sort"
      responses:
        "200":
          description: Segmentos.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/Segment"
                  meta:
                    type: object
                    properties:
                      pagination:
                        $ref: "#/components/schemas/Pagination"
        "403":
          $ref: "#/components/responses/Forbidden"
    post:
      operationId: createSegment
      tags:
        - Segmentos
      summary: Criar segmento
      description: |
        Cria um segmento dinâmico. `filters.conditions` não pode ser vazio.
        O `contactCount` é calculado na criação.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - data
              properties:
                data:
                  type: object
                  required:
                    - name
                    - filters
                  properties:
                    name:
                      type: string
                      example: Leads SP com e-mail
                    description:
                      type: string
                    filters:
                      $ref: "#/components/schemas/SegmentFilters"
                    active:
                      type: boolean
      responses:
        "200":
          description: Segmento criado.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: "#/components/schemas/Segment"
        "400":
          description: "`Filtros são obrigatórios` — `filters.conditions` vazio ou ausente."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
  /segments/{id}:
    parameters:
      - $ref: "#/components/parameters/TenantHeader"
      - name: id
        in: path
        required: true
        description: documentId do segmento.
        schema:
          type: string
    get:
      operationId: getSegment
      tags:
        - Segmentos
      summary: Buscar segmento
      responses:
        "200":
          description: O segmento.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: "#/components/schemas/Segment"
        "404":
          $ref: "#/components/responses/NotFound"
    put:
      operationId: updateSegment
      tags:
        - Segmentos
      summary: Atualizar segmento
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                data:
                  type: object
                  properties:
                    name:
                      type: string
                    description:
                      type: string
                    filters:
                      $ref: "#/components/schemas/SegmentFilters"
                    active:
                      type: boolean
      responses:
        "200":
          description: Segmento atualizado.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: "#/components/schemas/Segment"
        "404":
          $ref: "#/components/responses/NotFound"
    delete:
      operationId: deleteSegment
      tags:
        - Segmentos
      summary: Excluir segmento
      responses:
        "200":
          description: Segmento excluído.
        "404":
          $ref: "#/components/responses/NotFound"
  /segments/preview:
    parameters:
      - $ref: "#/components/parameters/TenantHeader"
    post:
      operationId: previewSegment
      tags:
        - Segmentos
      summary: Pré-visualizar audiência
      description: |
        Resolve um conjunto de regras **sem salvar o segmento** — ideal para testar
        filtros antes de criar.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - filters
              properties:
                filters:
                  $ref: "#/components/schemas/SegmentFilters"
                page:
                  type: integer
                  default: 1
                pageSize:
                  type: integer
                  default: 20
            example:
              filters:
                logic: AND
                conditions:
                  - field: city
                    op: eq
                    value: São Paulo
                  - field: email
                    op: notEmpty
              page: 1
              pageSize: 20
      responses:
        "200":
          description: Contatos que casam com as regras.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      contacts:
                        type: array
                        items:
                          $ref: "#/components/schemas/ContactRef"
                      total:
                        type: integer
                      page:
                        type: integer
                      pageSize:
                        type: integer
        "400":
          $ref: "#/components/responses/BadRequest"
  /segments/{id}/resolve:
    parameters:
      - $ref: "#/components/parameters/TenantHeader"
      - name: id
        in: path
        required: true
        schema:
          type: string
    post:
      operationId: resolveSegment
      tags:
        - Segmentos
      summary: Resolver segmento (listar contatos)
      description: Retorna os contatos que casam com as regras do segmento salvo, paginados.
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                page:
                  type: integer
                  default: 1
                pageSize:
                  type: integer
                  default: 50
      responses:
        "200":
          description: Contatos do segmento.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      contacts:
                        type: array
                        items:
                          $ref: "#/components/schemas/ContactRef"
                      total:
                        type: integer
                      page:
                        type: integer
                      pageSize:
                        type: integer
        "404":
          $ref: "#/components/responses/NotFound"
  /segments/{id}/count:
    parameters:
      - $ref: "#/components/parameters/TenantHeader"
      - name: id
        in: path
        required: true
        schema:
          type: string
    get:
      operationId: countSegment
      tags:
        - Segmentos
      summary: Contar contatos do segmento
      responses:
        "200":
          description: Contagem atual.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      count:
                        type: integer
                        example: 231
        "404":
          $ref: "#/components/responses/NotFound"
  /segments/filter-options:
    parameters:
      - $ref: "#/components/parameters/TenantHeader"
    get:
      operationId: getSegmentFilterOptions
      tags:
        - Segmentos
      summary: Opções de filtro disponíveis
      description: Campos, operadores e valores disponíveis para montar regras de segmento (inclui campos customizados da organização).
      responses:
        "200":
          description: Opções de filtro.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
  /campaigns:
    parameters:
      - $ref: "#/components/parameters/TenantHeader"
    get:
      operationId: listCampaigns
      tags:
        - Campanhas
      summary: Listar campanhas
      description: Lista as campanhas da organização, com segmento e canal populados. Estatísticas de campanhas em andamento são recalculadas.
      responses:
        "200":
          description: Campanhas.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/Campaign"
        "403":
          $ref: "#/components/responses/Forbidden"
    post:
      operationId: createCampaign
      tags:
        - Campanhas
      summary: Criar campanha
      description: |
        Cria uma campanha de disparo em massa para um segmento.

        - `channel`: `whatsapp`, `email` ou `sms`.
        - Para `email`, `messageConfig` exige `subject` e `html`.
        - Com `scheduledAt`, a campanha nasce `scheduled`; sem, nasce `draft`
          (dispare com `POST /campaigns/{id}/trigger`).
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - data
              properties:
                data:
                  type: object
                  required:
                    - name
                    - segment
                    - channel
                    - messageConfig
                  properties:
                    name:
                      type: string
                      example: Reativação julho
                    description:
                      type: string
                    segment:
                      type: string
                      description: documentId do segmento alvo.
                    channel:
                      type: string
                      enum:
                        - whatsapp
                        - email
                        - sms
                    chatChannel:
                      type: string
                      description: documentId do canal de envio (obrigatório para WhatsApp).
                    messageConfig:
                      type: object
                      description: |
                        Conteúdo da mensagem. Para e-mail: `{ subject, html, text? }`.
                        Para WhatsApp: template e variáveis conforme o canal.
                      additionalProperties: true
                    scheduledAt:
                      type: string
                      format: date-time
      responses:
        "200":
          description: Campanha criada.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: "#/components/schemas/Campaign"
        "400":
          $ref: "#/components/responses/BadRequest"
  /campaigns/{id}:
    parameters:
      - $ref: "#/components/parameters/TenantHeader"
      - name: id
        in: path
        required: true
        description: documentId da campanha.
        schema:
          type: string
    get:
      operationId: getCampaign
      tags:
        - Campanhas
      summary: Buscar campanha
      responses:
        "200":
          description: A campanha.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: "#/components/schemas/Campaign"
        "404":
          $ref: "#/components/responses/NotFound"
    put:
      operationId: updateCampaign
      tags:
        - Campanhas
      summary: Atualizar campanha
      description: Campanhas `processing` ou `completed` não podem ser editadas (400).
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                data:
                  type: object
      responses:
        "200":
          description: Campanha atualizada.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: "#/components/schemas/Campaign"
        "400":
          $ref: "#/components/responses/BadRequest"
        "404":
          $ref: "#/components/responses/NotFound"
    delete:
      operationId: deleteCampaign
      tags:
        - Campanhas
      summary: Excluir campanha
      responses:
        "200":
          description: Excluída.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
        "404":
          $ref: "#/components/responses/NotFound"
  /campaigns/{id}/stats:
    parameters:
      - $ref: "#/components/parameters/TenantHeader"
      - name: id
        in: path
        required: true
        schema:
          type: string
    get:
      operationId: getCampaignStats
      tags:
        - Campanhas
      summary: Estatísticas da campanha
      description: |
        Contadores agregados de envio e engajamento. `total/pending/sent/failed/skipped`
        são recalculados dos logs; `delivered/opened/clicked/bounced` vêm dos eventos
        do provedor de e-mail.
      responses:
        "200":
          description: Estatísticas.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: "#/components/schemas/CampaignStats"
              example:
                data:
                  total: 500
                  pending: 120
                  sent: 350
                  failed: 20
                  skipped: 10
                  delivered: 300
                  opened: 180
                  clicked: 45
                  bounced: 5
        "404":
          $ref: "#/components/responses/NotFound"
      x-codeSamples:
        - lang: cURL
          label: cURL
          source: |
            curl "https://api.perpetuus.com.br/api/campaigns/CAMPAIGN_ID/stats" \
              -H "Authorization: Bearer SEU_TOKEN" \
              -H "x-tenant-id: acme"
  /campaigns/{id}/trigger:
    parameters:
      - $ref: "#/components/parameters/TenantHeader"
      - name: id
        in: path
        required: true
        schema:
          type: string
    post:
      operationId: triggerCampaign
      tags:
        - Campanhas
      summary: Disparar campanha
      description: Inicia o envio imediatamente. Sujeito a rate limit reforçado.
      responses:
        "200":
          description: Envio iniciado.
        "400":
          description: Transição de estado inválida (ex. campanha já em processamento).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          $ref: "#/components/responses/NotFound"
  /campaigns/{id}/pause:
    parameters:
      - $ref: "#/components/parameters/TenantHeader"
      - name: id
        in: path
        required: true
        schema:
          type: string
    post:
      operationId: pauseCampaign
      tags:
        - Campanhas
      summary: Pausar campanha
      responses:
        "200":
          description: Campanha pausada.
        "400":
          $ref: "#/components/responses/BadRequest"
        "404":
          $ref: "#/components/responses/NotFound"
  /campaigns/{id}/resume:
    parameters:
      - $ref: "#/components/parameters/TenantHeader"
      - name: id
        in: path
        required: true
        schema:
          type: string
    post:
      operationId: resumeCampaign
      tags:
        - Campanhas
      summary: Retomar campanha
      responses:
        "200":
          description: Campanha retomada.
        "400":
          $ref: "#/components/responses/BadRequest"
        "404":
          $ref: "#/components/responses/NotFound"
  /campaigns/{id}/cancel:
    parameters:
      - $ref: "#/components/parameters/TenantHeader"
      - name: id
        in: path
        required: true
        schema:
          type: string
    post:
      operationId: cancelCampaign
      tags:
        - Campanhas
      summary: Cancelar campanha
      responses:
        "200":
          description: Campanha cancelada.
        "400":
          $ref: "#/components/responses/BadRequest"
        "404":
          $ref: "#/components/responses/NotFound"
  /campaigns/{id}/retry:
    parameters:
      - $ref: "#/components/parameters/TenantHeader"
      - name: id
        in: path
        required: true
        schema:
          type: string
    post:
      operationId: retryCampaign
      tags:
        - Campanhas
      summary: Reenviar falhas
      description: Recoloca na fila os envios com status `failed`.
      responses:
        "200":
          description: Reenvio iniciado.
        "400":
          $ref: "#/components/responses/BadRequest"
        "404":
          $ref: "#/components/responses/NotFound"
  /tasks:
    parameters:
      - $ref: "#/components/parameters/TenantHeader"
    get:
      operationId: listTasks
      tags:
        - Tarefas
      summary: Listar tarefas
      description: |
        Lista tarefas dos boards acessíveis ao usuário, ordenadas por `dueDate:asc`.
        Filtre por board com `filters[board][documentId]`.
      parameters:
        - $ref: "#/components/parameters/Sort"
        - name: limit
          in: query
          schema:
            type: integer
        - name: offset
          in: query
          schema:
            type: integer
      responses:
        "200":
          description: Array de tarefas (flat), com `card` e `assignees` populados.
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/Task"
        "403":
          $ref: "#/components/responses/Forbidden"
    post:
      operationId: createTask
      tags:
        - Tarefas
      summary: Criar tarefa
      description: |
        Cria uma tarefa. Se `assignees` for omitido, a tarefa é atribuída ao criador.
        `taskStatus` usa os status configurados no board (`taskStatuses`, padrão
        `pending`, `in-progress`, `completed`).
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - data
              properties:
                data:
                  type: object
                  required:
                    - name
                  properties:
                    name:
                      type: string
                      maxLength: 255
                      example: Ligar para o cliente
                    description:
                      type: string
                    dueDate:
                      type: string
                      format: date-time
                    type:
                      type: string
                      enum:
                        - reminder
                        - follow-up
                        - action
                        - other
                    taskStatus:
                      type: string
                    card:
                      type: string
                      description: documentId do card vinculado (opcional).
                    board:
                      type: string
                      description: documentId do board (opcional se `card` for enviado).
                    assignees:
                      type: array
                      items:
                        type: string
                      description: documentIds de memberships.
      responses:
        "200":
          description: Tarefa criada (flat).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Task"
        "400":
          $ref: "#/components/responses/BadRequest"
        "404":
          $ref: "#/components/responses/NotFound"
  /tasks/my-tasks:
    parameters:
      - $ref: "#/components/parameters/TenantHeader"
    get:
      operationId: getMyTasks
      tags:
        - Tarefas
      summary: Minhas tarefas
      description: Tarefas atribuídas ao usuário autenticado, em boards que ele acessa, ordenadas por vencimento.
      responses:
        "200":
          description: Array de tarefas (flat).
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/Task"
  /boards/{boardId}/tasks:
    parameters:
      - $ref: "#/components/parameters/TenantHeader"
      - name: boardId
        in: path
        required: true
        schema:
          type: string
    get:
      operationId: getBoardTasks
      tags:
        - Tarefas
      summary: Tarefas do board
      responses:
        "200":
          description: Array de tarefas do board (flat).
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/Task"
        "404":
          $ref: "#/components/responses/NotFound"
  /tasks/{id}:
    parameters:
      - $ref: "#/components/parameters/TenantHeader"
      - name: id
        in: path
        required: true
        description: documentId da tarefa.
        schema:
          type: string
    get:
      operationId: getTask
      tags:
        - Tarefas
      summary: Buscar tarefa
      responses:
        "200":
          description: A tarefa (flat).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Task"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
    put:
      operationId: updateTask
      tags:
        - Tarefas
      summary: Atualizar tarefa
      description: |
        Atualização parcial. Marcar um status de conclusão (ex. `completed`) preenche
        `completedAt` automaticamente. Operadores só editam tarefas onde são assignees.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                data:
                  type: object
                  properties:
                    name:
                      type: string
                    taskStatus:
                      type: string
                      example: completed
                    dueDate:
                      type: string
                      format: date-time
                    assignees:
                      type: array
                      items:
                        type: string
      responses:
        "200":
          description: Tarefa atualizada (flat).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Task"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
    delete:
      operationId: deleteTask
      tags:
        - Tarefas
      summary: Excluir tarefa
      responses:
        "200":
          description: Tarefa excluída.
        "404":
          $ref: "#/components/responses/NotFound"
  /comments:
    parameters:
      - $ref: "#/components/parameters/TenantHeader"
    get:
      operationId: listComments
      tags:
        - Comentários
      summary: Listar comentários
      description: |
        Lista comentários dos cards acessíveis. Para os comentários de um card,
        use `filters[card][documentId]=CARD_ID` (caminho otimizado).
      parameters:
        - name: filters[card][documentId]
          in: query
          description: documentId do card.
          schema:
            type: string
      responses:
        "200":
          description: Comentários (com `author` populado). Com filtro de card, envelope `{ data, meta.pagination }`.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/Comment"
                  meta:
                    type: object
    post:
      operationId: createComment
      tags:
        - Comentários
      summary: Comentar em um card
      description: |
        Cria um comentário. `card` é obrigatório.

        - **Menções**: envie `mentions` (documentIds de memberships) ou deixe que a
          API as detecte no conteúdo; menções inválidas retornam 400.
        - Com `contentFormat: html`, o conteúdo é sanitizado.
        - Links no conteúdo são detectados e salvos em `metadata.links`.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - data
              properties:
                data:
                  type: object
                  required:
                    - card
                    - content
                  properties:
                    card:
                      type: string
                      description: documentId do card.
                    content:
                      type: string
                    contentFormat:
                      type: string
                      enum:
                        - plain
                        - html
                      default: plain
                    mentions:
                      type: array
                      items:
                        type: string
            example:
              data:
                card: cd1a2b3c
                content: Cliente confirmou reunião para sexta.
      responses:
        "200":
          description: Comentário criado (flat).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Comment"
        "400":
          $ref: "#/components/responses/BadRequest"
        "403":
          $ref: "#/components/responses/Forbidden"
  /comments/{id}:
    parameters:
      - $ref: "#/components/parameters/TenantHeader"
      - name: id
        in: path
        required: true
        schema:
          type: string
    get:
      operationId: getComment
      tags:
        - Comentários
      summary: Buscar comentário
      responses:
        "200":
          description: O comentário (flat).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Comment"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
    put:
      operationId: updateComment
      tags:
        - Comentários
      summary: Editar comentário
      description: Apenas o autor pode editar. Novas menções são notificadas.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                data:
                  type: object
                  properties:
                    content:
                      type: string
                    contentFormat:
                      type: string
                      enum:
                        - plain
                        - html
      responses:
        "200":
          description: Comentário atualizado.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Comment"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
    delete:
      operationId: deleteComment
      tags:
        - Comentários
      summary: Excluir comentário
      description: Apenas o autor pode excluir.
      responses:
        "200":
          description: Comentário excluído.
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
  /teams:
    parameters:
      - $ref: "#/components/parameters/TenantHeader"
    get:
      operationId: listTeams
      tags:
        - Times
      summary: Listar times
      description: Lista os times da organização, com líder e membros populados. Requer o módulo de times habilitado no plano.
      responses:
        "200":
          description: Array de times (flat).
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/Team"
        "403":
          $ref: "#/components/responses/Forbidden"
    post:
      operationId: createTeam
      tags:
        - Times
      summary: Criar time
      description: Apenas administradores. `leader` é obrigatório.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - data
              properties:
                data:
                  type: object
                  required:
                    - name
                    - leader
                  properties:
                    name:
                      type: string
                      example: Time SP
                    description:
                      type: string
                    leader:
                      type: string
                      description: documentId do membership líder.
                    color:
                      type: string
                      default: "#3B82F6"
                    active:
                      type: boolean
      responses:
        "200":
          description: Time criado (flat).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Team"
        "400":
          $ref: "#/components/responses/BadRequest"
        "403":
          $ref: "#/components/responses/Forbidden"
  /teams/{id}:
    parameters:
      - $ref: "#/components/parameters/TenantHeader"
      - name: id
        in: path
        required: true
        description: documentId do time.
        schema:
          type: string
    get:
      operationId: getTeam
      tags:
        - Times
      summary: Buscar time
      responses:
        "200":
          description: O time (flat).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Team"
        "404":
          $ref: "#/components/responses/NotFound"
    put:
      operationId: updateTeam
      tags:
        - Times
      summary: Atualizar time
      description: Permitido para administradores e para o líder do time.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                data:
                  type: object
      responses:
        "200":
          description: Time atualizado.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Team"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
    delete:
      operationId: deleteTeam
      tags:
        - Times
      summary: Excluir time
      description: Apenas administradores. Times com contatos vinculados não podem ser excluídos (400).
      responses:
        "200":
          description: Time excluído.
        "400":
          $ref: "#/components/responses/BadRequest"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
  /teams/{id}/members:
    parameters:
      - $ref: "#/components/parameters/TenantHeader"
      - name: id
        in: path
        required: true
        schema:
          type: string
    post:
      operationId: addTeamMember
      tags:
        - Times
      summary: Adicionar membro ao time
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - data
              properties:
                data:
                  type: object
                  required:
                    - membershipId
                  properties:
                    membershipId:
                      type: string
                      description: documentId do membership.
      responses:
        "200":
          description: Membro adicionado.
        "400":
          $ref: "#/components/responses/BadRequest"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
  /teams/{id}/members/{membershipId}:
    parameters:
      - $ref: "#/components/parameters/TenantHeader"
      - name: id
        in: path
        required: true
        schema:
          type: string
      - name: membershipId
        in: path
        required: true
        schema:
          type: string
    delete:
      operationId: removeTeamMember
      tags:
        - Times
      summary: Remover membro do time
      responses:
        "200":
          description: Membro removido.
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
  /quotes:
    parameters:
      - $ref: "#/components/parameters/TenantHeader"
    get:
      operationId: listQuotes
      tags:
        - Orçamentos
      summary: Listar orçamentos
      description: |
        Lista orçamentos da organização. Filtre com `filters[quoteStatus]`,
        `filters[card]` ou `filters[contact]` (documentIds).
      parameters:
        - name: filters[quoteStatus]
          in: query
          schema:
            type: string
            enum:
              - draft
              - active
              - won
              - lost
        - name: filters[card]
          in: query
          schema:
            type: string
        - name: filters[contact]
          in: query
          schema:
            type: string
        - $ref: "#/components/parameters/Sort"
        - name: limit
          in: query
          schema:
            type: integer
            default: 50
        - name: offset
          in: query
          schema:
            type: integer
      responses:
        "200":
          description: Array de orçamentos (flat), com card, contato e itens populados.
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/Quote"
    post:
      operationId: createQuote
      tags:
        - Orçamentos
      summary: Criar orçamento
      description: |
        Cria um orçamento. `items` é obrigatório (mínimo 1).

        - `number`, `subtotal` e `total` são **calculados no servidor** — valores
          enviados são ignorados.
        - `updateCardValue: true` atualiza o `value` do card com o total.
        - Itens abaixo do preço mínimo (`minPrice`) do produto exigem a permissão
          de override de preço (400 caso contrário).
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - items
              properties:
                items:
                  type: array
                  minItems: 1
                  items:
                    $ref: "#/components/schemas/QuoteItemInput"
                card:
                  type: string
                  description: documentId do card.
                contact:
                  type: string
                  description: documentId do contato.
                quoteStatus:
                  type: string
                  enum:
                    - draft
                    - active
                    - won
                    - lost
                discountType:
                  type: string
                  enum:
                    - percentage
                    - fixed
                discountValue:
                  type: number
                currency:
                  type: string
                  default: BRL
                validUntil:
                  type: string
                  format: date-time
                notes:
                  type: string
                pdfTemplate:
                  type: string
                  description: documentId do modelo de PDF.
                updateCardValue:
                  type: boolean
            example:
              card: cd1a2b3c
              items:
                - name: Consultoria estratégica
                  quantity: 1
                  unitPrice: 15000
                - product: pr9x8y7z
                  name: Plano anual
                  quantity: 1
                  periods: 12
                  unitPrice: 990
              discountType: percentage
              discountValue: 10
              updateCardValue: true
      responses:
        "200":
          description: Orçamento criado (flat), com número e totais calculados.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Quote"
        "400":
          description: Sem itens (`At least one item is required`) ou preço abaixo do mínimo sem permissão.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
  /quotes/{id}:
    parameters:
      - $ref: "#/components/parameters/TenantHeader"
      - name: id
        in: path
        required: true
        description: documentId do orçamento.
        schema:
          type: string
    get:
      operationId: getQuote
      tags:
        - Orçamentos
      summary: Buscar orçamento
      responses:
        "200":
          description: O orçamento (flat), com itens e produtos.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Quote"
        "404":
          $ref: "#/components/responses/NotFound"
    put:
      operationId: updateQuote
      tags:
        - Orçamentos
      summary: Atualizar orçamento
      description: Atualização parcial. Totais são recalculados quando itens/desconto mudam.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                items:
                  type: array
                  items:
                    $ref: "#/components/schemas/QuoteItemInput"
                discountType:
                  type: string
                  enum:
                    - percentage
                    - fixed
                discountValue:
                  type: number
                quoteStatus:
                  type: string
                  enum:
                    - draft
                    - active
                    - won
                    - lost
                validUntil:
                  type: string
                  format: date-time
                notes:
                  type: string
                updateCardValue:
                  type: boolean
      responses:
        "200":
          description: Orçamento atualizado.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Quote"
        "400":
          $ref: "#/components/responses/BadRequest"
        "404":
          $ref: "#/components/responses/NotFound"
    delete:
      operationId: deleteQuote
      tags:
        - Orçamentos
      summary: Excluir orçamento
      responses:
        "200":
          description: Orçamento excluído.
        "404":
          $ref: "#/components/responses/NotFound"
  /quotes/{id}/activate:
    parameters:
      - $ref: "#/components/parameters/TenantHeader"
      - name: id
        in: path
        required: true
        schema:
          type: string
    post:
      operationId: activateQuote
      tags:
        - Orçamentos
      summary: Ativar orçamento
      description: |
        Marca o orçamento como `active` e rebaixa os demais orçamentos ativos do
        mesmo card para `draft`. Pode atualizar o valor do card conforme as
        configurações do board.
      responses:
        "200":
          description: Orçamento ativado.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Quote"
        "400":
          description: Orçamento já está ativo.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          $ref: "#/components/responses/NotFound"
  /quotes/{id}/duplicate:
    parameters:
      - $ref: "#/components/parameters/TenantHeader"
      - name: id
        in: path
        required: true
        schema:
          type: string
    post:
      operationId: duplicateQuote
      tags:
        - Orçamentos
      summary: Duplicar orçamento
      description: Cria uma cópia `draft` com novo número e versão incrementada.
      responses:
        "200":
          description: Cópia criada.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Quote"
        "404":
          $ref: "#/components/responses/NotFound"
  /quotes/{id}/pdf:
    parameters:
      - $ref: "#/components/parameters/TenantHeader"
      - name: id
        in: path
        required: true
        schema:
          type: string
    get:
      operationId: getQuotePdf
      tags:
        - Orçamentos
      summary: Gerar PDF do orçamento
      parameters:
        - name: templateId
          in: query
          description: documentId de um modelo de PDF específico (opcional).
          schema:
            type: string
      responses:
        "200":
          description: PDF do orçamento (download).
          content:
            application/pdf:
              schema:
                type: string
                format: binary
        "404":
          $ref: "#/components/responses/NotFound"
      x-codeSamples:
        - lang: cURL
          label: cURL
          source: |
            curl "https://api.perpetuus.com.br/api/quotes/QUOTE_ID/pdf" \
              -H "Authorization: Bearer SEU_TOKEN" \
              -H "x-tenant-id: acme" \
              --output orcamento.pdf
  /boards/{boardId}/cards/{cardId}/quotes:
    parameters:
      - $ref: "#/components/parameters/TenantHeader"
      - name: boardId
        in: path
        required: true
        schema:
          type: string
      - name: cardId
        in: path
        required: true
        schema:
          type: string
    get:
      operationId: listCardQuotes
      tags:
        - Orçamentos
      summary: Orçamentos de um card
      responses:
        "200":
          description: Array de orçamentos do card (flat).
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/Quote"
        "400":
          description: Card não pertence a este board.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          $ref: "#/components/responses/NotFound"
    post:
      operationId: createCardQuote
      tags:
        - Orçamentos
      summary: Criar orçamento para um card
      description: |
        Mesmo corpo de `POST /quotes`, com `card` injetado automaticamente.
        O `contact` é herdado do card se não for enviado.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - items
              properties:
                items:
                  type: array
                  minItems: 1
                  items:
                    $ref: "#/components/schemas/QuoteItemInput"
                discountType:
                  type: string
                discountValue:
                  type: number
                updateCardValue:
                  type: boolean
      responses:
        "200":
          description: Orçamento criado.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Quote"
        "400":
          $ref: "#/components/responses/BadRequest"
        "404":
          $ref: "#/components/responses/NotFound"
  /products:
    parameters:
      - $ref: "#/components/parameters/TenantHeader"
    get:
      operationId: listProducts
      tags:
        - Produtos
      summary: Listar produtos
      description: |
        Lista o catálogo. `filters[name]` busca em nome **e** SKU.

        Com `pagination[page]`/`[pageSize]`, a resposta usa `{ data, meta.pagination }`;
        sem paginação, retorna array flat (até 100 itens).
      parameters:
        - name: filters[productType]
          in: query
          schema:
            type: string
            enum:
              - physical
              - digital
              - service
        - name: filters[billingType]
          in: query
          schema:
            type: string
            enum:
              - one_time
              - recurring
        - name: filters[active]
          in: query
          schema:
            type: boolean
        - name: filters[categories][documentId]
          in: query
          schema:
            type: string
        - name: filters[name]
          in: query
          description: Busca em nome e SKU.
          schema:
            type: string
        - $ref: "#/components/parameters/PaginationPage"
        - $ref: "#/components/parameters/PaginationPageSize"
        - $ref: "#/components/parameters/Sort"
      responses:
        "200":
          description: Produtos.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/Product"
                  meta:
                    type: object
                    properties:
                      pagination:
                        $ref: "#/components/schemas/Pagination"
    post:
      operationId: createProduct
      tags:
        - Produtos
      summary: Criar produto
      description: "`name`, `sku` e `price` são obrigatórios. O SKU é único por organização."
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - data
              properties:
                data:
                  type: object
                  required:
                    - name
                    - sku
                    - price
                  properties:
                    name:
                      type: string
                      example: Plano Pro
                    sku:
                      type: string
                      example: PLANO-PRO
                    price:
                      type: number
                      example: 990
                    salePrice:
                      type: number
                    minPrice:
                      type: number
                      description: Piso de venda — orçamentos abaixo exigem permissão de override.
                    productType:
                      type: string
                      enum:
                        - physical
                        - digital
                        - service
                    billingType:
                      type: string
                      enum:
                        - one_time
                        - recurring
                    description:
                      type: string
                    shortDescription:
                      type: string
                      maxLength: 500
                    currency:
                      type: string
                      default: BRL
                    active:
                      type: boolean
                    categories:
                      type: array
                      items:
                        type: string
                      description: documentIds de categorias.
      responses:
        "200":
          description: Produto criado (flat).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Product"
        "400":
          description: Nome/SKU/preço ausentes ou SKU duplicado.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
  /products/{id}:
    parameters:
      - $ref: "#/components/parameters/TenantHeader"
      - name: id
        in: path
        required: true
        description: documentId do produto.
        schema:
          type: string
    get:
      operationId: getProduct
      tags:
        - Produtos
      summary: Buscar produto
      responses:
        "200":
          description: O produto (flat), com imagem, galeria e categorias.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Product"
        "404":
          $ref: "#/components/responses/NotFound"
    put:
      operationId: updateProduct
      tags:
        - Produtos
      summary: Atualizar produto
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                data:
                  type: object
      responses:
        "200":
          description: Produto atualizado.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Product"
        "400":
          $ref: "#/components/responses/BadRequest"
        "404":
          $ref: "#/components/responses/NotFound"
    delete:
      operationId: deleteProduct
      tags:
        - Produtos
      summary: Excluir produto
      responses:
        "200":
          description: Produto excluído.
        "404":
          $ref: "#/components/responses/NotFound"
  /product-categories:
    parameters:
      - $ref: "#/components/parameters/TenantHeader"
    get:
      operationId: listProductCategories
      tags:
        - Produtos
      summary: Listar categorias
      responses:
        "200":
          description: Array de categorias (flat), com categoria pai populada.
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/ProductCategory"
    post:
      operationId: createProductCategory
      tags:
        - Produtos
      summary: Criar categoria
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - data
              properties:
                data:
                  type: object
                  required:
                    - name
                  properties:
                    name:
                      type: string
                      maxLength: 100
                    description:
                      type: string
                    color:
                      type: string
                      default: "#3b82f6"
                    parent:
                      type: string
                      description: documentId da categoria pai (para hierarquia).
      responses:
        "200":
          description: Categoria criada.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ProductCategory"
        "400":
          $ref: "#/components/responses/BadRequest"
  /product-categories/{id}:
    parameters:
      - $ref: "#/components/parameters/TenantHeader"
      - name: id
        in: path
        required: true
        schema:
          type: string
    get:
      operationId: getProductCategory
      tags:
        - Produtos
      summary: Buscar categoria
      responses:
        "200":
          description: A categoria (flat).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ProductCategory"
        "404":
          $ref: "#/components/responses/NotFound"
    put:
      operationId: updateProductCategory
      tags:
        - Produtos
      summary: Atualizar categoria
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                data:
                  type: object
      responses:
        "200":
          description: Categoria atualizada.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ProductCategory"
        "404":
          $ref: "#/components/responses/NotFound"
    delete:
      operationId: deleteProductCategory
      tags:
        - Produtos
      summary: Excluir categoria
      responses:
        "200":
          description: Categoria excluída.
        "404":
          $ref: "#/components/responses/NotFound"
  /pdf-templates:
    parameters:
      - $ref: "#/components/parameters/TenantHeader"
    get:
      operationId: listPdfTemplates
      tags:
        - Modelos de PDF
      summary: Listar modelos de PDF
      responses:
        "200":
          description: Modelos da organização.
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/PdfTemplate"
    post:
      operationId: createPdfTemplate
      tags:
        - Modelos de PDF
      summary: Criar modelo de PDF
      description: |
        Cria um modelo com layout em blocos (cabeçalho, corpo e rodapé) e estilos
        customizados. Usado na geração de PDFs de orçamentos.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - data
              properties:
                data:
                  type: object
                  required:
                    - name
                  properties:
                    name:
                      type: string
                      maxLength: 100
                    description:
                      type: string
                    templateType:
                      type: string
                      enum:
                        - quote
                        - invoice
                        - contract
                        - report
                      default: quote
                    layout:
                      type: object
                      description: Estrutura `{ pageSize, orientation, margins, header, body, footer }` com blocos.
                    styles:
                      type: object
                      description: "Cores e fontes: `{ primaryColor, secondaryColor, fontFamily, fontSize }`."
      responses:
        "200":
          description: Modelo criado.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PdfTemplate"
        "400":
          $ref: "#/components/responses/BadRequest"
  /pdf-templates/{id}:
    parameters:
      - $ref: "#/components/parameters/TenantHeader"
      - name: id
        in: path
        required: true
        schema:
          type: string
    get:
      operationId: getPdfTemplate
      tags:
        - Modelos de PDF
      summary: Buscar modelo
      responses:
        "200":
          description: O modelo.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PdfTemplate"
        "404":
          $ref: "#/components/responses/NotFound"
    put:
      operationId: updatePdfTemplate
      tags:
        - Modelos de PDF
      summary: Atualizar modelo
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                data:
                  type: object
      responses:
        "200":
          description: Modelo atualizado.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PdfTemplate"
        "404":
          $ref: "#/components/responses/NotFound"
    delete:
      operationId: deletePdfTemplate
      tags:
        - Modelos de PDF
      summary: Excluir modelo
      responses:
        "200":
          description: Modelo excluído.
        "404":
          $ref: "#/components/responses/NotFound"
  /pdf-templates/{id}/duplicate:
    parameters:
      - $ref: "#/components/parameters/TenantHeader"
      - name: id
        in: path
        required: true
        schema:
          type: string
    post:
      operationId: duplicatePdfTemplate
      tags:
        - Modelos de PDF
      summary: Duplicar modelo
      responses:
        "200":
          description: Cópia criada.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PdfTemplate"
        "404":
          $ref: "#/components/responses/NotFound"
  /pdf-templates/preview:
    parameters:
      - $ref: "#/components/parameters/TenantHeader"
    post:
      operationId: previewPdfTemplate
      tags:
        - Modelos de PDF
      summary: Pré-visualizar modelo
      description: Gera um PDF de amostra a partir de um layout, sem salvar.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - layout
              properties:
                layout:
                  type: object
                styles:
                  type: object
      responses:
        "200":
          description: PDF de pré-visualização (inline).
          content:
            application/pdf:
              schema:
                type: string
                format: binary
        "400":
          $ref: "#/components/responses/BadRequest"
  /automations:
    parameters:
      - $ref: "#/components/parameters/TenantHeader"
    get:
      operationId: listAutomations
      tags:
        - Automações
      summary: Listar automações
      description: |
        **BETA — API avançada (risco de breaking change).**

        Lista automações da organização. Filtre por board com `filters[board][documentId]`.
      responses:
        "200":
          description: Lista de automações (envelope Strapi ou array, conforme query).
        "403":
          $ref: "#/components/responses/Forbidden"
    post:
      operationId: createAutomation
      tags:
        - Automações
      summary: Criar automação
      description: |
        **BETA.** Cria rascunho. `active: true` é rejeitado no create — publique antes de ativar.

        Obrigatórios: `name`, `trigger` (`{ type, filters? }`), `actions[]` (≥1 com `type` + `order`).
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - data
              properties:
                data:
                  type: object
                  required:
                    - name
                    - trigger
                    - actions
                  properties:
                    name:
                      type: string
                      example: Lead novo → atribuir e notificar
                    description:
                      type: string
                    board:
                      type: string
                      description: documentId do board (opcional).
                    trigger:
                      type: object
                      properties:
                        type:
                          type: string
                          example: card.created
                    actions:
                      type: array
                      minItems: 1
                      items:
                        type: object
                    active:
                      type: boolean
                      default: false
            example:
              data:
                name: Lead novo → tarefa
                trigger:
                  type: card.created
                actions:
                  - type: task.create
                    order: 0
                active: false
      responses:
        "200":
          description: Automação criada (inactive).
        "400":
          $ref: "#/components/responses/BadRequest"
  /automations/{id}:
    parameters:
      - $ref: "#/components/parameters/TenantHeader"
      - name: id
        in: path
        required: true
        schema:
          type: string
    get:
      operationId: getAutomation
      tags:
        - Automações
      summary: Buscar automação
      description: "**BETA.**"
      responses:
        "200":
          description: Automação.
        "404":
          $ref: "#/components/responses/NotFound"
    put:
      operationId: updateAutomation
      tags:
        - Automações
      summary: Atualizar automação
      description: |
        **BETA.** Atualização parcial. `active: true` só é aceito se já existir versão publicada.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                data:
                  type: object
      responses:
        "200":
          description: Automação atualizada.
        "400":
          $ref: "#/components/responses/BadRequest"
    delete:
      operationId: deleteAutomation
      tags:
        - Automações
      summary: Excluir automação
      description: "**BETA.** Remove a automação e versões associadas."
      responses:
        "200":
          description: Excluída.
        "404":
          $ref: "#/components/responses/NotFound"
  /automations/{id}/toggle:
    parameters:
      - $ref: "#/components/parameters/TenantHeader"
      - name: id
        in: path
        required: true
        schema:
          type: string
    post:
      operationId: toggleAutomation
      tags:
        - Automações
      summary: Ativar / desativar
      description: |
        **BETA.** Alterna `active`. Ativar exige publish prévio.
      responses:
        "200":
          description: Estado atualizado.
        "400":
          $ref: "#/components/responses/BadRequest"
  /automations/{id}/duplicate:
    parameters:
      - $ref: "#/components/parameters/TenantHeader"
      - name: id
        in: path
        required: true
        schema:
          type: string
    post:
      operationId: duplicateAutomation
      tags:
        - Automações
      summary: Duplicar automação
      description: "**BETA.** Cria cópia inactive."
      responses:
        "200":
          description: Cópia criada.
  /automations/{id}/validate-graph:
    parameters:
      - $ref: "#/components/parameters/TenantHeader"
      - name: id
        in: path
        required: true
        schema:
          type: string
    post:
      operationId: validateAutomationGraph
      tags:
        - Automações
      summary: Validar grafo
      description: |
        **BETA.** Valida `nodes`/`edges` (ou `canvasData`) sem publicar. Sem side-effect.
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                nodes:
                  type: array
                edges:
                  type: array
                canvasData:
                  type: object
      responses:
        "200":
          description: Resultado da validação.
        "400":
          $ref: "#/components/responses/BadRequest"
  /automations/{id}/publish:
    parameters:
      - $ref: "#/components/parameters/TenantHeader"
      - name: id
        in: path
        required: true
        schema:
          type: string
    post:
      operationId: publishAutomation
      tags:
        - Automações
      summary: Publicar versão
      description: |
        **BETA — efeito colateral.** Cria `automation-version` publicada, arquiva versões
        anteriores e no primeiro publish pode ativar a automação.
      requestBody:
        content:
          application/json:
            schema:
              type: object
              description: Grafo opcional; se omitido, usa o canvas atual.
      responses:
        "200":
          description: Versão publicada.
        "400":
          $ref: "#/components/responses/BadRequest"
      x-codeSamples:
        - lang: cURL
          source: |
            curl -X POST "https://api.perpetuus.com.br/api/automations/AUTO_ID/publish" \
              -H "Authorization: Bearer SEU_TOKEN" \
              -H "x-tenant-id: acme" \
              -H "Content-Type: application/json" \
              -d '{}'
  /automations/{id}/test:
    parameters:
      - $ref: "#/components/parameters/TenantHeader"
      - name: id
        in: path
        required: true
        schema:
          type: string
    post:
      operationId: testAutomation
      tags:
        - Automações
      summary: Testar automação
      description: |
        **BETA — perigoso.** `dryRun` default `true`. Com `dryRun: false` as ações
        executam de verdade (mensagens, HTTP, tarefas…). Rate limit reforçado.
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                dryRun:
                  type: boolean
                  default: true
                event:
                  type: object
                  properties:
                    type:
                      type: string
                    payload:
                      type: object
                    boardId:
                      type: string
            example:
              dryRun: true
              event:
                type: card.created
                boardId: bd1a2b3c
      responses:
        "200":
          description: Resultado do teste.
        "400":
          $ref: "#/components/responses/BadRequest"
  /automations/{id}/runs:
    parameters:
      - $ref: "#/components/parameters/TenantHeader"
      - name: id
        in: path
        required: true
        schema:
          type: string
    get:
      operationId: listAutomationRuns
      tags:
        - Automações
      summary: Listar execuções
      description: "**BETA.** Histórico de runs da automação."
      responses:
        "200":
          description: Lista de runs.
  /automations/{id}/runs/{runId}:
    parameters:
      - $ref: "#/components/parameters/TenantHeader"
      - name: id
        in: path
        required: true
        schema:
          type: string
      - name: runId
        in: path
        required: true
        schema:
          type: string
    get:
      operationId: getAutomationRun
      tags:
        - Automações
      summary: Detalhe de execução
      description: "**BETA.** Inclui step runs quando disponíveis."
      responses:
        "200":
          description: Run detalhado.
        "404":
          $ref: "#/components/responses/NotFound"
  /automation-logs:
    parameters:
      - $ref: "#/components/parameters/TenantHeader"
    get:
      operationId: listAutomationLogs
      tags:
        - Automações
      summary: Listar logs
      description: "**BETA.** Logs agregados de automações."
      responses:
        "200":
          description: Logs.
  /automation-logs/{id}:
    parameters:
      - $ref: "#/components/parameters/TenantHeader"
      - name: id
        in: path
        required: true
        schema:
          type: string
    get:
      operationId: getAutomationLog
      tags:
        - Automações
      summary: Buscar log
      description: "**BETA.**"
      responses:
        "200":
          description: Log.
        "404":
          $ref: "#/components/responses/NotFound"
  /automations/{automationId}/logs/stats:
    parameters:
      - $ref: "#/components/parameters/TenantHeader"
      - name: automationId
        in: path
        required: true
        schema:
          type: string
    get:
      operationId: getAutomationLogStats
      tags:
        - Automações
      summary: Estatísticas de logs
      description: "**BETA.** Contagens agregadas por status/período."
      responses:
        "200":
          description: Stats.
  /chat-channels:
    parameters:
      - $ref: "#/components/parameters/TenantHeader"
    get:
      operationId: listChatChannels
      tags:
        - Canais
      summary: Listar canais
      description: |
        **BETA — API avançada (risco de breaking change).**

        Lista canais da organização (`whatsapp`, `instagram`, `telegram`, `email`, `web`, `api`).
      responses:
        "200":
          description: Canais (credenciais sensíveis mascaradas).
        "403":
          $ref: "#/components/responses/Forbidden"
    post:
      operationId: createChatChannel
      tags:
        - Canais
      summary: Criar canal
      description: |
        **BETA — efeito colateral.** Cria o canal e provisiona a inbox associada.
        Sujeito a limite de canais do plano. Obrigatórios: `name`, `channelType`.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - data
              properties:
                data:
                  type: object
                  required:
                    - name
                    - channelType
                  properties:
                    name:
                      type: string
                      example: WhatsApp Comercial
                    channelType:
                      type: string
                      enum:
                        - whatsapp
                        - instagram
                        - telegram
                        - email
                        - web
                        - api
                    credentials:
                      type: object
                      description: Credenciais do provedor (nunca retornadas completas).
            example:
              data:
                name: WhatsApp Comercial
                channelType: whatsapp
                credentials:
                  accessToken: EAAB...
                  phoneNumberId: "123456"
                  businessAccountId: "789"
      responses:
        "200":
          description: Canal criado.
        "400":
          $ref: "#/components/responses/BadRequest"
  /chat-channels/whatsapp-embedded-signup:
    parameters:
      - $ref: "#/components/parameters/TenantHeader"
    post:
      operationId: createWhatsAppFromEmbeddedSignup
      tags:
        - Canais
      summary: Criar WhatsApp via Embedded Signup
      description: |
        **BETA.** Finaliza o fluxo Embedded Signup da Meta. Obrigatórios: `code`, `wabaId`, `phoneNumberId`.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - code
                - wabaId
                - phoneNumberId
              properties:
                code:
                  type: string
                wabaId:
                  type: string
                phoneNumberId:
                  type: string
                name:
                  type: string
      responses:
        "200":
          description: Canal WhatsApp criado.
        "400":
          $ref: "#/components/responses/BadRequest"
  /chat-channels/email-config:
    parameters:
      - $ref: "#/components/parameters/TenantHeader"
    get:
      operationId: getEmailChannelConfig
      tags:
        - Canais
      summary: Configuração de e-mail
      description: "**BETA.** Config global de envio de e-mail da organização."
      responses:
        "200":
          description: Configuração.
    put:
      operationId: saveEmailChannelConfig
      tags:
        - Canais
      summary: Salvar configuração de e-mail
      description: "**BETA.**"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
      responses:
        "200":
          description: Config salva.
  /chat-channels/{id}:
    parameters:
      - $ref: "#/components/parameters/TenantHeader"
      - name: id
        in: path
        required: true
        schema:
          type: string
    get:
      operationId: getChatChannel
      tags:
        - Canais
      summary: Buscar canal
      description: "**BETA.**"
      responses:
        "200":
          description: Canal.
        "404":
          $ref: "#/components/responses/NotFound"
    put:
      operationId: updateChatChannel
      tags:
        - Canais
      summary: Atualizar canal
      description: "**BETA.**"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                data:
                  type: object
      responses:
        "200":
          description: Canal atualizado.
    delete:
      operationId: deleteChatChannel
      tags:
        - Canais
      summary: Excluir canal
      description: "**BETA — efeito colateral.** Remove o canal e recursos vinculados no provedor."
      responses:
        "200":
          description: Excluído.
  /chat-channels/{id}/health:
    parameters:
      - $ref: "#/components/parameters/TenantHeader"
      - name: id
        in: path
        required: true
        schema:
          type: string
    get:
      operationId: getChatChannelHealth
      tags:
        - Canais
      summary: Saúde do canal
      description: "**BETA.** Status de conectividade/credenciais."
      responses:
        "200":
          description: Health check.
  /chat-channels/{id}/preferences:
    parameters:
      - $ref: "#/components/parameters/TenantHeader"
      - name: id
        in: path
        required: true
        schema:
          type: string
    get:
      operationId: getChatChannelPreferences
      tags:
        - Canais
      summary: Preferências do canal
      description: "**BETA.**"
      responses:
        "200":
          description: Preferências.
    patch:
      operationId: updateChatChannelPreferences
      tags:
        - Canais
      summary: Atualizar preferências
      description: "**BETA.**"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
      responses:
        "200":
          description: Preferências atualizadas.
  /chat-channels/{id}/test:
    parameters:
      - $ref: "#/components/parameters/TenantHeader"
      - name: id
        in: path
        required: true
        schema:
          type: string
    post:
      operationId: testChatChannel
      tags:
        - Canais
      summary: Testar conexão
      description: "**BETA.** Valida credenciais junto ao provedor."
      responses:
        "200":
          description: Resultado do teste.
  /chat-channels/{id}/templates:
    parameters:
      - $ref: "#/components/parameters/TenantHeader"
      - name: id
        in: path
        required: true
        schema:
          type: string
    get:
      operationId: listChannelTemplates
      tags:
        - Canais
      summary: Listar templates WhatsApp
      description: |
        **BETA.** Lista templates HSM do WABA (proxy da Graph API). Só canais `whatsapp`.
      responses:
        "200":
          description: Templates.
    post:
      operationId: createChannelTemplate
      tags:
        - Canais
      summary: Criar template WhatsApp
      description: |
        **BETA — efeito colateral.** Cria template na Meta (`name`, `language`, `category`, `components`).
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - name
                - language
                - category
              properties:
                name:
                  type: string
                language:
                  type: string
                  example: pt_BR
                category:
                  type: string
                  example: MARKETING
                components:
                  type: array
                  items:
                    type: object
      responses:
        "200":
          description: Template criado/enviado para aprovação.
        "400":
          $ref: "#/components/responses/BadRequest"
    delete:
      operationId: deleteChannelTemplate
      tags:
        - Canais
      summary: Excluir template WhatsApp
      description: |
        **BETA — efeito colateral.** Remove na Meta. Query: `name` e/ou `hsm_id`.
      parameters:
        - name: name
          in: query
          schema:
            type: string
        - name: hsm_id
          in: query
          schema:
            type: string
      responses:
        "200":
          description: Template excluído.
  /chat-channels/{id}/templates/sync:
    parameters:
      - $ref: "#/components/parameters/TenantHeader"
      - name: id
        in: path
        required: true
        schema:
          type: string
    post:
      operationId: syncChannelTemplates
      tags:
        - Canais
      summary: Sincronizar templates
      description: "**BETA — efeito colateral.** Sincroniza templates do provedor com o canal."
      responses:
        "200":
          description: Sync concluído.
  /chat-channels/{id}/templates/upload-handle:
    parameters:
      - $ref: "#/components/parameters/TenantHeader"
      - name: id
        in: path
        required: true
        schema:
          type: string
    post:
      operationId: uploadChannelTemplateMedia
      tags:
        - Canais
      summary: Upload de mídia para template
      description: "**BETA.** Multipart `file` → `media_handle` para componentes de template."
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required:
                - file
              properties:
                file:
                  type: string
                  format: binary
      responses:
        "200":
          description: Handle de mídia.
  /boards/{boardId}/chat-channels:
    parameters:
      - $ref: "#/components/parameters/TenantHeader"
      - name: boardId
        in: path
        required: true
        schema:
          type: string
    get:
      operationId: listBoardChatChannels
      tags:
        - Canais
      summary: Canais vinculados ao board
      description: "**BETA.**"
      responses:
        "200":
          description: Vínculos canal↔board.
    post:
      operationId: linkBoardChatChannel
      tags:
        - Canais
      summary: Vincular canal ao board
      description: "**BETA.** Associa um canal existente ao board."
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                data:
                  type: object
                  required:
                    - chatChannel
                  properties:
                    chatChannel:
                      type: string
                      description: documentId do canal.
      responses:
        "200":
          description: Vínculo criado.
        "400":
          $ref: "#/components/responses/BadRequest"
  /boards/{boardId}/chat-channels/{linkId}:
    parameters:
      - $ref: "#/components/parameters/TenantHeader"
      - name: boardId
        in: path
        required: true
        schema:
          type: string
      - name: linkId
        in: path
        required: true
        schema:
          type: string
    put:
      operationId: updateBoardChatChannelLink
      tags:
        - Canais
      summary: Atualizar vínculo
      description: "**BETA.**"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
      responses:
        "200":
          description: Vínculo atualizado.
    delete:
      operationId: unlinkBoardChatChannel
      tags:
        - Canais
      summary: Desvincular canal
      description: "**BETA.**"
      responses:
        "200":
          description: Vínculo removido.
  /meta-lead-integrations:
    parameters:
      - $ref: "#/components/parameters/TenantHeader"
    get:
      operationId: listMetaLeadIntegrations
      tags:
        - Meta Lead Ads
      summary: Listar integrações Lead Ads
      description: |
        **BETA — API avançada (risco de breaking change).**

        Tokens de página/usuário são privados e mascarados nas respostas.
      responses:
        "200":
          description: Integrações.
        "403":
          $ref: "#/components/responses/Forbidden"
    post:
      operationId: createMetaLeadIntegration
      tags:
        - Meta Lead Ads
      summary: Criar integração
      description: |
        **BETA — efeito colateral.** Valida o page token e **inscreve a página em webhooks leadgen**.
        Obrigatórios: `name`, `facebookPageId`, `pageAccessToken`.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - data
              properties:
                data:
                  type: object
                  required:
                    - name
                    - facebookPageId
                    - pageAccessToken
                  properties:
                    name:
                      type: string
                      example: Página Acme Ads
                    facebookPageId:
                      type: string
                    pageAccessToken:
                      type: string
                    webhookVerifyToken:
                      type: string
                    active:
                      type: boolean
                      default: true
      responses:
        "200":
          description: Integração criada e inscrita.
        "400":
          $ref: "#/components/responses/BadRequest"
  /meta-lead-integrations/{id}:
    parameters:
      - $ref: "#/components/parameters/TenantHeader"
      - name: id
        in: path
        required: true
        schema:
          type: string
    get:
      operationId: getMetaLeadIntegration
      tags:
        - Meta Lead Ads
      summary: Buscar integração
      description: "**BETA.**"
      responses:
        "200":
          description: Integração.
        "404":
          $ref: "#/components/responses/NotFound"
    put:
      operationId: updateMetaLeadIntegration
      tags:
        - Meta Lead Ads
      summary: Atualizar integração
      description: "**BETA.** Pode re-inscrever webhooks ao atualizar tokens."
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                data:
                  type: object
      responses:
        "200":
          description: Atualizada.
    delete:
      operationId: deleteMetaLeadIntegration
      tags:
        - Meta Lead Ads
      summary: Excluir integração
      description: "**BETA.**"
      responses:
        "200":
          description: Excluída.
  /meta-lead-integrations/{id}/leadgen-forms:
    parameters:
      - $ref: "#/components/parameters/TenantHeader"
      - name: id
        in: path
        required: true
        schema:
          type: string
    get:
      operationId: listMetaLeadgenForms
      tags:
        - Meta Lead Ads
      summary: Formulários Leadgen da página
      description: "**BETA.** Lista forms disponíveis na página conectada."
      responses:
        "200":
          description: Forms.
  /meta-lead-integrations/{id}/leadgen-forms/{formId}/questions:
    parameters:
      - $ref: "#/components/parameters/TenantHeader"
      - name: id
        in: path
        required: true
        schema:
          type: string
      - name: formId
        in: path
        required: true
        schema:
          type: string
    get:
      operationId: getMetaLeadgenFormQuestions
      tags:
        - Meta Lead Ads
      summary: Perguntas do formulário
      description: "**BETA.** Use para montar `fieldMapping` nas form routes."
      responses:
        "200":
          description: Questions.
  /meta-lead-integrations/{id}/campaign-performance:
    parameters:
      - $ref: "#/components/parameters/TenantHeader"
      - name: id
        in: path
        required: true
        schema:
          type: string
    get:
      operationId: getMetaLeadCampaignPerformance
      tags:
        - Meta Lead Ads
      summary: Performance de campanhas
      description: "**BETA.** Métricas agregadas das campanhas vinculadas."
      responses:
        "200":
          description: Performance.
  /meta-lead-integrations/{id}/user-token:
    parameters:
      - $ref: "#/components/parameters/TenantHeader"
      - name: id
        in: path
        required: true
        schema:
          type: string
    post:
      operationId: connectMetaLeadUserToken
      tags:
        - Meta Lead Ads
      summary: Conectar user access token
      description: |
        **BETA — sensível.** Grava `userAccessToken`, valida Ads e atualiza credentials.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - userAccessToken
              properties:
                userAccessToken:
                  type: string
      responses:
        "200":
          description: Token conectado.
        "400":
          $ref: "#/components/responses/BadRequest"
  /meta-lead-form-routes:
    parameters:
      - $ref: "#/components/parameters/TenantHeader"
    post:
      operationId: createMetaLeadFormRoute
      tags:
        - Meta Lead Ads
      summary: Criar roteamento de formulário
      description: |
        **BETA.** Mapeia um form Leadgen → board + `fieldMapping`.

        Obrigatórios: `metaLeadIntegrationDocumentId`, `targetBoardDocumentId`,
        `facebookFormId`, `fieldMapping.mappings[]` (≥1).
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - data
              properties:
                data:
                  type: object
                  required:
                    - metaLeadIntegrationDocumentId
                    - targetBoardDocumentId
                    - facebookFormId
                    - fieldMapping
                  properties:
                    name:
                      type: string
                    metaLeadIntegrationDocumentId:
                      type: string
                    targetBoardDocumentId:
                      type: string
                    facebookFormId:
                      type: string
                    priority:
                      type: integer
                    active:
                      type: boolean
                    fieldMapping:
                      type: object
                      properties:
                        mappings:
                          type: array
                          items:
                            type: object
            example:
              data:
                name: Form Contato → Funil
                metaLeadIntegrationDocumentId: mli1a2b3c
                targetBoardDocumentId: bd1a2b3c
                facebookFormId: "120000"
                fieldMapping:
                  mappings:
                    - source: email
                      target: email
                    - source: full_name
                      target: name
      responses:
        "200":
          description: Form route criada.
        "400":
          $ref: "#/components/responses/BadRequest"
  /meta-lead-form-routes/{id}:
    parameters:
      - $ref: "#/components/parameters/TenantHeader"
      - name: id
        in: path
        required: true
        schema:
          type: string
    get:
      operationId: getMetaLeadFormRoute
      tags:
        - Meta Lead Ads
      summary: Buscar form route
      description: "**BETA.**"
      responses:
        "200":
          description: Form route.
        "404":
          $ref: "#/components/responses/NotFound"
    put:
      operationId: updateMetaLeadFormRoute
      tags:
        - Meta Lead Ads
      summary: Atualizar form route
      description: "**BETA.**"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                data:
                  type: object
      responses:
        "200":
          description: Atualizada.
    delete:
      operationId: deleteMetaLeadFormRoute
      tags:
        - Meta Lead Ads
      summary: Excluir form route
      description: "**BETA.**"
      responses:
        "200":
          description: Excluída.
  /meta-lead/facebook-oauth/start:
    parameters:
      - $ref: "#/components/parameters/TenantHeader"
    post:
      operationId: startMetaLeadOAuth
      tags:
        - Meta Lead Ads
      summary: Iniciar OAuth Facebook
      description: |
        **BETA.** Retorna URL de autorização (`url`, `state`) para redirecionar o usuário.
        O callback do browser é interno (`/meta-lead/facebook-oauth/callback`) e não faz
        parte do contrato público.
  /meta-lead/facebook-oauth/complete:
    parameters:
      - $ref: "#/components/parameters/TenantHeader"
    post:
      operationId: completeMetaLeadOAuth
      tags:
        - Meta Lead Ads
      summary: Completar OAuth
      description: "**BETA.** Finaliza o fluxo após o usuário autorizar."
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
      responses:
        "200":
          description: OAuth concluído / integração pronta.
  /meta-lead/facebook-oauth/pages:
    parameters:
      - $ref: "#/components/parameters/TenantHeader"
    get:
      operationId: listMetaLeadOAuthPages
      tags:
        - Meta Lead Ads
      summary: Páginas disponíveis (OAuth)
      description: "**BETA.** Páginas Facebook acessíveis com o token da sessão OAuth."
      responses:
        "200":
          description: Pages.
  /meta-lead/facebook-oauth/leadgen-forms:
    parameters:
      - $ref: "#/components/parameters/TenantHeader"
    get:
      operationId: listMetaLeadOAuthForms
      tags:
        - Meta Lead Ads
      summary: Forms Leadgen (OAuth)
      description: "**BETA.**"
      parameters:
        - name: pageId
          in: query
          schema:
            type: string
      responses:
        "200":
          description: Forms.
  /meta-lead/facebook-oauth/leadgen-form-questions:
    parameters:
      - $ref: "#/components/parameters/TenantHeader"
    get:
      operationId: getMetaLeadOAuthFormQuestions
      tags:
        - Meta Lead Ads
      summary: Questions (OAuth)
      description: "**BETA.**"
      parameters:
        - name: formId
          in: query
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Questions.
  /email-templates:
    parameters:
      - $ref: "#/components/parameters/TenantHeader"
    get:
      operationId: listEmailTemplates
      tags:
        - E-mail marketing
      summary: Listar templates de e-mail
      description: |
        **BETA — API avançada (risco de breaking change).**
      responses:
        "200":
          description: Templates.
    post:
      operationId: createEmailTemplate
      tags:
        - E-mail marketing
      summary: Criar template de e-mail
      description: |
        **BETA.** Obrigatórios: `name`, `subject`, `html` (ou `designJson.html`).
        `category`: `marketing` | `transactional` | `notification`.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - data
              properties:
                data:
                  type: object
                  required:
                    - name
                    - subject
                    - html
                  properties:
                    name:
                      type: string
                    subject:
                      type: string
                    html:
                      type: string
                    preheader:
                      type: string
                    text:
                      type: string
                    category:
                      type: string
                      enum:
                        - marketing
                        - transactional
                        - notification
                    editorMode:
                      type: string
                      enum:
                        - visual
                        - html
            example:
              data:
                name: Boas-vindas
                subject: Bem-vindo à {{organization.name}}
                html: <h1>Olá {{contact.name}}</h1>
                category: marketing
      responses:
        "200":
          description: Template criado.
        "400":
          $ref: "#/components/responses/BadRequest"
  /email-templates/{id}:
    parameters:
      - $ref: "#/components/parameters/TenantHeader"
      - name: id
        in: path
        required: true
        schema:
          type: string
    get:
      operationId: getEmailTemplate
      tags:
        - E-mail marketing
      summary: Buscar template
      description: "**BETA.**"
      responses:
        "200":
          description: Template.
        "404":
          $ref: "#/components/responses/NotFound"
    put:
      operationId: updateEmailTemplate
      tags:
        - E-mail marketing
      summary: Atualizar template
      description: "**BETA.**"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                data:
                  type: object
      responses:
        "200":
          description: Atualizado.
    delete:
      operationId: deleteEmailTemplate
      tags:
        - E-mail marketing
      summary: Excluir template
      description: "**BETA.**"
      responses:
        "200":
          description: Excluído.
  /email-templates/preview:
    parameters:
      - $ref: "#/components/parameters/TenantHeader"
    post:
      operationId: previewEmailTemplate
      tags:
        - E-mail marketing
      summary: Pré-visualizar template
      description: |
        **BETA.** Renderiza HTML/subject sem enviar. Aceita corpo inline ou `templateId`.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                templateId:
                  type: string
                subject:
                  type: string
                html:
                  type: string
                sampleContact:
                  type: object
      responses:
        "200":
          description: Preview renderizado.
  /email-templates/send-test:
    parameters:
      - $ref: "#/components/parameters/TenantHeader"
    post:
      operationId: sendTestEmailTemplate
      tags:
        - E-mail marketing
      summary: Enviar e-mail de teste
      description: |
        **BETA — efeito colateral.** Envia e-mail **real** para `to`. Rate limit reforçado.
        Requer feature de campanhas no plano.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - to
              properties:
                to:
                  type: string
                  format: email
                toName:
                  type: string
                templateId:
                  type: string
                subject:
                  type: string
                html:
                  type: string
                chatChannelId:
                  type: string
            example:
              to: voce@empresa.com
              templateId: et1a2b3c
      responses:
        "200":
          description: Enviado.
        "400":
          $ref: "#/components/responses/BadRequest"
  /email-assets:
    parameters:
      - $ref: "#/components/parameters/TenantHeader"
    get:
      operationId: listEmailAssets
      tags:
        - E-mail marketing
      summary: Listar assets de e-mail
      description: "**BETA.** Imagens/arquivos usados em templates."
      responses:
        "200":
          description: Assets.
  /email-assets/upload:
    parameters:
      - $ref: "#/components/parameters/TenantHeader"
    post:
      operationId: uploadEmailAsset
      tags:
        - E-mail marketing
      summary: Upload de asset
      description: "**BETA.** Multipart upload."
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              properties:
                files:
                  type: string
                  format: binary
      responses:
        "200":
          description: Asset criado.
  /email-assets/{id}:
    parameters:
      - $ref: "#/components/parameters/TenantHeader"
      - name: id
        in: path
        required: true
        schema:
          type: string
    get:
      operationId: getEmailAsset
      tags:
        - E-mail marketing
      summary: Buscar asset
      description: "**BETA.**"
      responses:
        "200":
          description: Asset.
        "404":
          $ref: "#/components/responses/NotFound"
    delete:
      operationId: deleteEmailAsset
      tags:
        - E-mail marketing
      summary: Excluir asset
      description: "**BETA.**"
      responses:
        "200":
          description: Excluído.
  /distribution-queues:
    parameters:
      - $ref: "#/components/parameters/TenantHeader"
    get:
      operationId: listDistributionQueues
      tags:
        - Distribuição
      summary: Listar filas de distribuição
      description: |
        **BETA — API avançada.** Filas ativas atribuem cards automaticamente (round-robin).
      responses:
        "200":
          description: Filas.
    post:
      operationId: createDistributionQueue
      tags:
        - Distribuição
      summary: Criar fila
      description: |
        **BETA.** Obrigatórios: `name`, `board` (documentId). Opcional: `members[]`, `rules`, `schedule`.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - data
              properties:
                data:
                  type: object
                  required:
                    - name
                    - board
                  properties:
                    name:
                      type: string
                      example: Fila SP
                    board:
                      type: string
                    active:
                      type: boolean
                    members:
                      type: array
                      items:
                        type: string
                    rules:
                      type: object
                    schedule:
                      type: object
      responses:
        "200":
          description: Fila criada.
        "400":
          $ref: "#/components/responses/BadRequest"
  /distribution-queues/{id}:
    parameters:
      - $ref: "#/components/parameters/TenantHeader"
      - name: id
        in: path
        required: true
        schema:
          type: string
    get:
      operationId: getDistributionQueue
      tags:
        - Distribuição
      summary: Buscar fila
      description: "**BETA.**"
      responses:
        "200":
          description: Fila.
        "404":
          $ref: "#/components/responses/NotFound"
    put:
      operationId: updateDistributionQueue
      tags:
        - Distribuição
      summary: Atualizar fila
      description: "**BETA.**"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                data:
                  type: object
      responses:
        "200":
          description: Atualizada.
    delete:
      operationId: deleteDistributionQueue
      tags:
        - Distribuição
      summary: Excluir fila
      description: "**BETA.**"
      responses:
        "200":
          description: Excluída.
  /board-saved-filters:
    parameters:
      - $ref: "#/components/parameters/TenantHeader"
    get:
      operationId: listBoardSavedFilters
      tags:
        - Filtros salvos
      summary: Listar filtros salvos
      description: |
        **BETA.** Filtre por board com `filters[board][documentId]`.
      responses:
        "200":
          description: Filtros.
    post:
      operationId: createBoardSavedFilter
      tags:
        - Filtros salvos
      summary: Criar filtro salvo
      description: |
        **BETA.** Obrigatórios: `name`, `board`, `payload` (com `version`).
        `scope`: `personal` (default) | `shared` (só admin/manager).
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - data
              properties:
                data:
                  type: object
                  required:
                    - name
                    - board
                    - payload
                  properties:
                    name:
                      type: string
                      maxLength: 120
                    board:
                      type: string
                    scope:
                      type: string
                      enum:
                        - personal
                        - shared
                    isDefault:
                      type: boolean
                    payload:
                      type: object
                      required:
                        - version
                      properties:
                        version:
                          type: integer
                          example: 1
                        quick:
                          type: object
                        search:
                          type: string
                        advanced:
                          type: array
                          items:
                            type: object
            example:
              data:
                name: Meus atrasados
                board: bd1a2b3c
                scope: personal
                payload:
                  version: 1
                  quick:
                    assignee: me
                    dueTodayOnly: true
      responses:
        "200":
          description: Filtro criado.
        "400":
          $ref: "#/components/responses/BadRequest"
  /board-saved-filters/{id}:
    parameters:
      - $ref: "#/components/parameters/TenantHeader"
      - name: id
        in: path
        required: true
        schema:
          type: string
    get:
      operationId: getBoardSavedFilter
      tags:
        - Filtros salvos
      summary: Buscar filtro
      description: "**BETA.**"
      responses:
        "200":
          description: Filtro.
        "404":
          $ref: "#/components/responses/NotFound"
    put:
      operationId: updateBoardSavedFilter
      tags:
        - Filtros salvos
      summary: Atualizar filtro
      description: "**BETA.**"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                data:
                  type: object
      responses:
        "200":
          description: Atualizado.
    delete:
      operationId: deleteBoardSavedFilter
      tags:
        - Filtros salvos
      summary: Excluir filtro
      description: "**BETA.**"
      responses:
        "200":
          description: Excluído.
  /conversion-integrations:
    parameters:
      - $ref: "#/components/parameters/TenantHeader"
    get:
      operationId: listConversionIntegrations
      tags:
        - Conversões
      summary: Listar pixels / CAPI
      description: |
        **BETA — API avançada.** Credenciais mascaradas. Plataformas: `meta`, `google_ga4`.
      responses:
        "200":
          description: Integrações.
    post:
      operationId: createConversionIntegration
      tags:
        - Conversões
      summary: Criar integração de conversão
      description: |
        **BETA — efeito colateral.** Create **verifica síncrono** nas APIs Meta/GA4;
        falha = não cria.

        - Meta: `destinationId` = Pixel ID + `accessToken`
        - GA4: `destinationId` = Measurement ID + `apiSecret`
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - data
              properties:
                data:
                  type: object
                  required:
                    - name
                    - platform
                    - destinationId
                  properties:
                    name:
                      type: string
                    platform:
                      type: string
                      enum:
                        - meta
                        - google_ga4
                    destinationId:
                      type: string
                    accessToken:
                      type: string
                    apiSecret:
                      type: string
                    testEventCode:
                      type: string
                    active:
                      type: boolean
            example:
              data:
                name: Pixel Site
                platform: meta
                destinationId: "1234567890"
                accessToken: EAAB...
      responses:
        "200":
          description: Criada e verificada.
        "400":
          $ref: "#/components/responses/BadRequest"
  /conversion-integrations/{id}:
    parameters:
      - $ref: "#/components/parameters/TenantHeader"
      - name: id
        in: path
        required: true
        schema:
          type: string
    put:
      operationId: updateConversionIntegration
      tags:
        - Conversões
      summary: Atualizar integração
      description: "**BETA.**"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                data:
                  type: object
      responses:
        "200":
          description: Atualizada.
    delete:
      operationId: deleteConversionIntegration
      tags:
        - Conversões
      summary: Excluir integração
      description: "**BETA.**"
      responses:
        "200":
          description: Excluída.
  /conversion-integrations/{id}/verify:
    parameters:
      - $ref: "#/components/parameters/TenantHeader"
      - name: id
        in: path
        required: true
        schema:
          type: string
    post:
      operationId: verifyConversionIntegration
      tags:
        - Conversões
      summary: Revalidar credenciais
      description: |
        **BETA — efeito colateral.** Chama a API externa e atualiza `verificationStatus`
        (`verified` | `failed`).
      responses:
        "200":
          description: Resultado da verificação.
        "400":
          $ref: "#/components/responses/BadRequest"
