openapi: 3.0.3
info:
  title: 4.works API externa
  description: |
    API REST v1 para integrações de servidor a servidor. Autenticação por **Bearer Token**: Api Key (Administração >  Configurações).

    Base URL (típico): `https://app.4.works/api-externa/v1`

    Todas as listagens usam `page` e `per_page` (máx. 100). Resposta paginada inclui `meta.total`, `meta.page`, `meta.per_page`, `meta.total_pages`.

    Limite de taxa: por padrão **30 req/s por empresa**. Respostas incluem `X-RateLimit-Remaining`; `429` quando excedido.

    ## MCP (Model Context Protocol)

    Além do REST, esta API expõe um servidor **MCP** em `POST /mcp` (JSON-RPC 2.0) para conectar IAs
    externas (Claude, ChatGPT, Gemini, etc.). O MCP autentica via **Bearer Token** (Api Key) ou via
    **OAuth 2.1** (com PKCE + Dynamic Client Registration), permitindo que o conector autentique o
    usuário sem edição manual de arquivos de configuração. Endpoints OAuth:
    `/.well-known/oauth-authorization-server`, `/.well-known/oauth-protected-resource`,
    `/mcp/authorize` (consentimento), `POST /mcp/oauth/register`, `POST /mcp/oauth/token`, `POST /mcp/oauth/revoke`.
    O MCP expõe tools que executam os endpoints REST desta API, incluindo geração de sites, artigos de
    blog e imagens com a Otto IA.

    ## Eventos e notas/atas

    CRUD autenticado da comunidade (Bearer):

    - **Eventos**: `GET|POST /events`, `GET|PATCH|DELETE /events/{eventKey}`
    - **Notas/atas**: `GET|POST /events/{eventKey}/notes`, `GET|PATCH|DELETE /events/{eventKey}/notes/{noteKey}`

    `eventKey` aceita id numérico ou `fake_id`. Em GET de nota, `noteKey` também aceita `share_token`.
    Multi-empresa: passe `company_id` ou `company_fake_id` (query/body) para operar em outra comunidade do mesmo dono.
  version: "1.0.0"
servers:
  - url: /api-externa/v1
    description: Relativo ao host do app (ex. https://app.4.works)
security:
  - bearerAuth: []
tags:
  - name: Eventos
    description: CRUD de eventos da comunidade autenticada (Bearer). Não confundir com `GET /public/events` (aberto).
  - name: Notas de evento
    description: CRUD de notas/atas vinculadas a um evento (`/events/{eventKey}/notes`).
paths:
  /ping:
    get:
      summary: Health check (sem Bearer)
      security: []
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok: { type: boolean }
                  service: { type: string }
                  version: { type: string }
  /me:
    get:
      summary: Dados mínimos da empresa autenticada
      responses:
        "200":
          description: OK
        "401":
          description: Token inválido ou ausente
  /companies:
    get:
      summary: Listar as comunidades/empresas do mesmo dono (multi-empresa)
      description: |
        Lista TODAS as comunidades/empresas cujo proprietário
        (`companies.user_owner_id`) tem o mesmo e-mail do usuário resolvido pelo
        token atual (OAuth ou API key). Use para descobrir os `company_id` /
        `company_fake_id` disponíveis antes de direcionar uma ação a uma
        comunidade específica.

        Multi-empresa: em qualquer rota operacional (leads, deals, IA, etc.) você
        pode passar `company_id` **ou** `company_fake_id` (query ou body) para
        direcionar a chamada a OUTRA comunidade do mesmo dono. A titularidade é
        sempre validada — nunca é possível operar na comunidade de outra pessoa.
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      owner:
                        type: object
                        properties:
                          user_id: { type: integer }
                          email: { type: string }
                          name: { type: string }
                      total: { type: integer }
                      companies:
                        type: array
                        items:
                          type: object
                          properties:
                            company_id: { type: integer, example: 1234 }
                            fake_id: { type: string, example: "a1b2c3d4" }
                            name: { type: string }
                            display_name: { type: string }
                            plan_name: { type: string, example: "trial" }
                            theme: { type: string }
                            created_at: { type: string }
                            is_current: { type: boolean }
        "401":
          description: Token inválido ou ausente
        "403":
          description: Não foi possível resolver o proprietário do token
    post:
      summary: Criar nova comunidade/empresa no plano gratuito (trial)
      description: |
        Cria uma NOVA comunidade/empresa no plano gratuito (trial), tendo como
        dono o MESMO usuário do token atual (herda nome/e-mail/telefone do dono
        resolvido pelo token e o `theme` da comunidade autenticada). Depois de
        criada, opere nela passando `company_id` (ou `company_fake_id`) nas demais
        rotas.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name]
              properties:
                name: { type: string, description: "Nome da comunidade/empresa (obrigatório, máx 120)." }
                display_name: { type: string, description: "Nome de exibição (opcional; default = name)." }
                description: { type: string, description: "Descrição curta (opcional, máx 240)." }
      responses:
        "201":
          description: Comunidade criada
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      ok: { type: boolean, example: true }
                      company_id: { type: integer, example: 1234 }
                      fake_id: { type: string, example: "a1b2c3d4" }
                      name: { type: string }
                      plan_name: { type: string, example: "trial" }
                      theme: { type: string }
                      admin_user_id: { type: integer }
                      owner_email: { type: string }
                      public_url: { type: string, example: "https://app.4.works/a1b2c3d4" }
                      hint: { type: string, description: "Como direcionar chamadas à nova comunidade." }
        "401":
          description: Token inválido ou ausente
        "403":
          description: Não foi possível resolver o proprietário do token
        "422":
          description: "Campo obrigatório ausente (name)"
        "500":
          description: Falha ao criar a comunidade
  /companies/current:
    get:
      summary: Dados da comunidade/empresa ativa no contexto atual
      description: |
        Retorna os dados da comunidade atualmente ativa no contexto do token
        (a padrão do OAuth, ou a definida via override `company_id`/`company_fake_id`).
        O campo `overridden` indica se a empresa atual veio de um override
        multi-empresa.
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      company_id: { type: integer }
                      fake_id: { type: string }
                      name: { type: string }
                      display_name: { type: string }
                      plan_name: { type: string }
                      theme: { type: string }
                      overridden: { type: boolean }
                      owner:
                        type: object
                        properties:
                          user_id: { type: integer }
                          email: { type: string }
                          name: { type: string }
        "401":
          description: Token inválido ou ausente
  /auth/sso:
    post:
      summary: Gerar link de SSO (login de uso único) para o painel
      description: |
        Gera um link de **SSO** (`{app}/sso/<token>`) que permite entrar
        **logado** no painel da 4.works. O link é de **USO ÚNICO**: ao ser
        aberto no navegador, o `header.php` consome o token, autentica o usuário
        e depois o link deixa de funcionar.

        Usuário-alvo do SSO:
          - **Sem `email`** → o **admin/dono** da comunidade em questão
            (resolvido pelo e-mail do dono do token).
          - **Com `email`** → **qualquer membro** (não deletado) daquela
            comunidade. Como o dono do token controla a comunidade, pode gerar
            um link de acesso para qualquer um dos seus membros (útil para
            suporte/onboarding).

        Multi-empresa: passe `company_id` **ou** `company_fake_id` (query ou
        body) para gerar o SSO direto de OUTRA comunidade do mesmo dono. A
        titularidade é sempre validada.
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                email: { type: string, description: "Opcional: e-mail do membro que receberá o SSO. Se omitido, gera para o dono/admin da comunidade." }
                company_id: { type: integer, description: "Opcional: comunidade-alvo (multi-empresa do mesmo dono)." }
                company_fake_id: { type: string, description: "Opcional: alternativa ao company_id usando o fake_id." }
      responses:
        "200":
          description: Link de SSO gerado
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      ok: { type: boolean, example: true }
                      sso_url: { type: string, example: "https://app.4.works/sso/Ab3xK9mZ2q" }
                      token: { type: string }
                      one_time: { type: boolean, example: true }
                      company_id: { type: integer }
                      company_fake_id: { type: string }
                      user_id: { type: integer }
                      target_email: { type: string, description: "E-mail do usuário para quem o SSO foi gerado." }
                      is_owner: { type: boolean, description: "true quando o alvo é o próprio dono da comunidade." }
                      owner_email: { type: string }
                      overridden: { type: boolean }
                      hint: { type: string }
        "400":
          description: Comunidade inválida no contexto atual
        "401":
          description: Token inválido ou ausente
        "403":
          description: Não foi possível resolver o proprietário do token
        "404":
          description: Membro (email) não encontrado, ou usuário owner não resolvido na comunidade
        "500":
          description: Falha ao gerar o link de SSO
    get:
      summary: Gerar link de SSO (conveniência — mesma lógica do POST; aceita ?email=)
      parameters:
        - in: query
          name: email
          schema: { type: string }
          required: false
          description: "Opcional: e-mail do membro que receberá o SSO."
      responses:
        "200":
          description: Link de SSO gerado
        "401":
          description: Token inválido ou ausente
  /members:
    get:
      summary: Listar membros (usuários) da comunidade
      description: |
        Lista os membros da comunidade atual, paginado. Filtre por busca (`q`,
        casa em nome/e-mail) e por `access_type` (`admin` | `member`).

        Multi-empresa: passe `company_id` **ou** `company_fake_id` para listar
        membros de OUTRA comunidade do mesmo dono.
      parameters:
        - $ref: "#/components/parameters/page"
        - $ref: "#/components/parameters/perPage"
        - in: query
          name: q
          schema: { type: string }
          required: false
          description: "Busca por nome ou e-mail."
        - in: query
          name: access_type
          schema: { type: string, enum: [admin, member, user] }
          required: false
          description: "Filtra por tipo de acesso."
        - in: query
          name: company_id
          schema: { type: integer }
          required: false
        - in: query
          name: company_fake_id
          schema: { type: string }
          required: false
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      type: object
                      properties:
                        user_id: { type: integer }
                        fake_id: { type: string }
                        first_name: { type: string }
                        last_name: { type: string }
                        email: { type: string }
                        access_type: { type: string }
                        status: { type: integer }
                        created_at: { type: string, nullable: true }
                        last_login_at: { type: string, nullable: true }
                  pagination:
                    type: object
                    properties:
                      page: { type: integer }
                      per_page: { type: integer }
                      total: { type: integer }
                      total_pages: { type: integer }
        "401":
          description: Token inválido ou ausente
    post:
      summary: Criar novo membro (usuário) na comunidade
      description: |
        Cria um novo membro na comunidade atual, com acesso de **admin** ou de
        **membro comum** (`member`, alias de `user`). Retorna também um link de
        **SSO de uso único** (`sso_url`) para o membro entrar direto no painel.

        Se `send_invite=true`, dispara o e-mail de convite ao membro. Caso
        contrário, basta compartilhar o `sso_url`.

        Multi-empresa: passe `company_id` **ou** `company_fake_id` para criar em
        OUTRA comunidade do mesmo dono.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [email]
              properties:
                email: { type: string, description: "E-mail do novo membro (obrigatório)." }
                first_name: { type: string, description: "Primeiro nome (opcional; derivado do e-mail se vazio)." }
                last_name: { type: string, description: "Sobrenome (opcional)." }
                access_type: { type: string, enum: [admin, member], description: "Nível de acesso. Default: member." }
                send_invite: { type: boolean, description: "Se true, envia o e-mail de convite. Default: false." }
                company_id: { type: integer, description: "Opcional: comunidade-alvo (multi-empresa do mesmo dono)." }
                company_fake_id: { type: string, description: "Opcional: alternativa ao company_id usando o fake_id." }
      responses:
        "201":
          description: Membro criado
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      ok: { type: boolean, example: true }
                      user_id: { type: integer }
                      fake_id: { type: string }
                      email: { type: string }
                      first_name: { type: string }
                      last_name: { type: string }
                      access_type: { type: string }
                      company_id: { type: integer }
                      company_fake_id: { type: string }
                      invite_sent: { type: boolean }
                      sso_url: { type: string, example: "https://app.4.works/sso/Ab3xK9mZ2q" }
                      overridden: { type: boolean }
                      hint: { type: string }
        "409":
          description: Já existe um membro com este e-mail nesta comunidade
        "422":
          description: E-mail inválido
        "401":
          description: Token inválido ou ausente
        "500":
          description: Falha ao criar o membro
  /emails:
    post:
      summary: Enviar e-mail personalizado para um membro da comunidade
      description: |
        Envia um e-mail com **título** e **corpo** personalizados para um
        **membro existente** da comunidade. O destinatário PRECISA ser um membro
        da comunidade (não é possível enviar para endereços externos).

        O corpo (`body`) aceita **HTML**. Se não for um documento HTML completo,
        o layout padrão da 4.works (cabeçalho/rodapé + botão CTA opcional) é
        aplicado automaticamente. Para incluir um botão de call-to-action,
        informe `button_label` e `button_url`.

        Por padrão o e-mail é **enfileirado** (`send_now=false`) e enviado pelo
        processador de fila; passe `send_now=true` para envio imediato.

        Multi-empresa: passe `company_id` **ou** `company_fake_id` para enviar em
        nome de OUTRA comunidade do mesmo dono.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [email, title, body]
              properties:
                email: { type: string, description: "E-mail do membro destinatário (obrigatório; deve ser membro da comunidade)." }
                title: { type: string, description: "Assunto/título do e-mail (obrigatório)." }
                body: { type: string, description: "Corpo do e-mail. Pode conter HTML (obrigatório)." }
                button_label: { type: string, description: "Texto do botão CTA (opcional). Requer button_url." }
                button_url: { type: string, description: "URL do botão CTA (opcional; http/https)." }
                send_now: { type: boolean, description: "Se true, envia imediatamente; se false (default), enfileira." }
                company_id: { type: integer, description: "Opcional: comunidade-alvo (multi-empresa do mesmo dono)." }
                company_fake_id: { type: string, description: "Opcional: alternativa ao company_id usando o fake_id." }
      responses:
        "200":
          description: E-mail enviado/enfileirado
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      ok: { type: boolean, example: true }
                      to: { type: string, description: "E-mail do destinatário." }
                      user_id: { type: integer }
                      title: { type: string }
                      queued: { type: boolean, description: "true quando enfileirado; false quando enviado imediatamente." }
                      sent_now: { type: boolean }
                      has_cta: { type: boolean, description: "true quando o e-mail incluiu botão CTA." }
                      company_id: { type: integer }
                      company_fake_id: { type: string }
                      overridden: { type: boolean }
                      hint: { type: string }
        "404":
          description: Nenhum membro com este e-mail nesta comunidade
        "422":
          description: Parâmetros obrigatórios ausentes ou inválidos
        "401":
          description: Token inválido ou ausente
        "500":
          description: Falha ao enviar o e-mail
  /public/events:
    get:
      summary: Listar eventos públicos de todas as empresas (SEM Bearer)
      description: |
        Endpoint **aberto** — não requer Bearer token. Lista todos os eventos
        **futuros** que estão aprovados, não-ocultos e não-deletados de
        qualquer empresa ativa na plataforma, com paginação. Ordenado por
        `event_start_at ASC` (mais próximos primeiro).

        Por razões de segurança/privacidade desta rota pública, o payload
        **não** expõe `id` do evento nem qualquer dado da empresa (id,
        fake_id, nome). Use o campo `public_url` para enviar o usuário até a
        página standalone do evento — lá ele vê tudo (empresa, ingressos,
        formulário de inscrição etc.).

        **Rate limit por IP**: ~10 req/s (mais baixo que o autenticado).
        Respostas incluem `X-RateLimit-Remaining`; `429` quando excedido.
      security: []
      parameters:
        - $ref: "#/components/parameters/page"
        - $ref: "#/components/parameters/perPage"
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      type: object
                      properties:
                        title: { type: string, example: "Workshop de Marketing Digital" }
                        description: { type: string, nullable: true, description: "HTML removido, máx. 1500 chars." }
                        cover_url: { type: string, nullable: true, example: "https://vp2uploads.s3.sa-east-1.amazonaws.com/4w/events/cover.jpg" }
                        start_at: { type: string, nullable: true, example: "2026-06-15 19:00:00" }
                        start_at_iso: { type: string, nullable: true, example: "2026-06-15T19:00:00-03:00" }
                        end_at: { type: string, nullable: true, example: "2026-06-15 22:00:00" }
                        end_at_iso: { type: string, nullable: true, example: "2026-06-15T22:00:00-03:00" }
                        type: { type: string, nullable: true, enum: [on-line, presencial] }
                        location: { type: string, nullable: true, example: "Online" }
                        public_url: { type: string, example: "https://app.4.works/sites/eventos/1569-workshop-de-marketing-digital" }
                  meta:
                    type: object
                    properties:
                      total: { type: integer }
                      page: { type: integer }
                      per_page: { type: integer }
                      total_pages: { type: integer }
        "429":
          description: Rate limit excedido (10 req/s por IP). Veja header `Retry-After`.

  /events:
    get:
      tags: [Eventos]
      operationId: listEvents
      summary: Listar eventos da comunidade (autenticado)
      description: |
        Lista eventos da empresa autenticada. Por padrão retorna só **futuros**
        (`event_start_at >= NOW()`). Use `include_past=true` para incluir passados.
        Filtros: `q` (nome/local), `status` (`0` pendente, `1` aprovado, `-1` recusado).
        Aceita override multi-empresa via `company_id` / `company_fake_id`.
        Diferente de `GET /public/events` (rota aberta sem Bearer).
      parameters:
        - $ref: "#/components/parameters/page"
        - $ref: "#/components/parameters/perPage"
        - name: q
          in: query
          schema: { type: string }
          description: Busca por nome ou local
        - name: status
          in: query
          schema: { type: string, enum: ["0", "1", "-1"] }
        - name: include_past
          in: query
          schema: { type: boolean, default: false }
        - name: company_id
          in: query
          schema: { type: integer }
          description: Override multi-empresa (outra comunidade do mesmo dono)
        - name: company_fake_id
          in: query
          schema: { type: string }
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/Event" }
                  meta:
                    type: object
                    properties:
                      total: { type: integer }
                      page: { type: integer }
                      per_page: { type: integer }
                      total_pages: { type: integer }
        "401": { description: Token inválido ou ausente }
        "403": { description: company_id sem titularidade }
    post:
      tags: [Eventos]
      operationId: createEvent
      summary: Criar evento
      description: |
        Cria um evento na comunidade autenticada.

        - Obrigatórios: `title` (ou `event_name` / `name`) e `start_at` (ou `event_start_at`).
        - Se `end_at` omitido, assume início + 2h.
        - Default `status=1` (aprovado). Use `0` para pendente de moderação.
        - Consome a cota do plano (`events_monthly`) → `402` com `code: limit_exceeded` se estourar.
        - `tickets[]` opcional sincroniza tipos de ingresso (`events_ticket_types`).
        - Aliases aceitos: `place`/`location`, `type`/`event_type`, `cover_url`/`cover`, `color`/`event_color`.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [title, start_at]
              properties:
                title: { type: string, example: "Workshop de Marketing" }
                event_name: { type: string, description: "Alias de title" }
                name: { type: string, description: "Alias de title" }
                start_at: { type: string, example: "2026-09-15 19:00:00", description: "Datetime (Y-m-d H:i:s ou ISO)" }
                end_at: { type: string, nullable: true, example: "2026-09-15 21:00:00" }
                place: { type: string, example: "Online" }
                location: { type: string, description: "Alias de place" }
                type: { type: string, enum: [on-line, presencial, hibrido], default: presencial }
                description: { type: string, description: "HTML da descrição" }
                cover_url: { type: string, nullable: true, description: "URL http(s) da capa" }
                status: { type: integer, enum: [0, 1, -1], default: 1 }
                need_confirmation: { type: boolean, default: false }
                hidden: { type: boolean, default: false }
                color: { type: string, example: "#57563f", description: "HEX #RRGGBB" }
                event_color: { type: string, description: "Alias de color" }
                inscription_type: { type: string, enum: [plataforma, link_externo], default: plataforma }
                inscription_link: { type: string, nullable: true }
                event_inscription_link: { type: string, nullable: true }
                price: { type: number, description: "Preço legado (evento); preferir tickets[]" }
                total_seats: { type: integer }
                publish_as: { type: string, enum: [user, community], default: user }
                company_id: { type: integer, description: "Override multi-empresa" }
                company_fake_id: { type: string }
                tickets:
                  type: array
                  items:
                    type: object
                    properties:
                      title: { type: string }
                      price: { type: number }
                      total_seats: { type: integer }
                      is_paid: { type: boolean }
            examples:
              online:
                summary: Evento online simples
                value:
                  title: Workshop via API
                  start_at: "2026-09-15 19:00:00"
                  end_at: "2026-09-15 21:00:00"
                  type: on-line
                  place: Online
                  description: "<p>Evento criado pela API externa.</p>"
                  status: 1
              presencial_com_ingresso:
                summary: Presencial com ingresso
                value:
                  title: Meetup presencial
                  start_at: "2026-10-01 18:30:00"
                  type: presencial
                  place: "Av. Paulista, 1000 — SP"
                  tickets:
                    - title: Gratuito
                      price: 0
                      total_seats: 80
                      is_paid: false
      responses:
        "201":
          description: Criado
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok: { type: boolean }
                  data: { $ref: "#/components/schemas/Event" }
        "402":
          description: "Limite do plano (`events_monthly`) — `code: limit_exceeded`"
        "422": { description: Validação (title/start_at/cover_url inválidos) }
        "401": { description: Token inválido ou ausente }
        "403": { description: company_id sem titularidade }

  /events/{eventKey}:
    parameters:
      - name: eventKey
        in: path
        required: true
        schema: { type: string }
        description: ID numérico ou `fake_id` do evento
        example: "42"
    get:
      tags: [Eventos]
      operationId: getEvent
      summary: Obter evento
      description: Retorna o evento com `ticket_types` / `ticket_primary` quando disponíveis.
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/Event" }
        "404": { description: Não encontrado }
        "401": { description: Token inválido ou ausente }
    patch:
      tags: [Eventos]
      operationId: updateEvent
      summary: Atualizar evento (parcial)
      description: |
        Atualização parcial. Envie apenas os campos a alterar.
        Aceita os mesmos aliases do POST (`title`/`event_name`, `place`/`location`, etc.).
        Se enviar `tickets[]`, sincroniza os tipos de ingresso do evento.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                title: { type: string }
                start_at: { type: string }
                end_at: { type: string }
                place: { type: string }
                type: { type: string, enum: [on-line, presencial, hibrido] }
                description: { type: string }
                cover_url: { type: string }
                status: { type: integer, enum: [0, 1, -1] }
                need_confirmation: { type: boolean }
                hidden: { type: boolean }
                color: { type: string }
                event_color: { type: string }
                inscription_type: { type: string, enum: [plataforma, link_externo] }
                inscription_link: { type: string }
                tickets: { type: array, items: { type: object } }
            example:
              title: Workshop via API (editado)
              place: São Paulo
              status: 1
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok: { type: boolean }
                  data: { $ref: "#/components/schemas/Event" }
        "404": { description: Não encontrado }
        "422": { description: Validação }
        "401": { description: Token inválido ou ausente }
    delete:
      tags: [Eventos]
      operationId: deleteEvent
      summary: Remover evento (soft-delete)
      description: |
        Soft-delete (`events.deleted_at`). O evento deixa de aparecer nas listagens
        autenticadas e públicas. Não remove fisicamente inscritos/notas.
      responses:
        "200":
          description: Removido
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok: { type: boolean, example: true }
                  id: { type: integer }
                  fake_id: { type: string }
                  message: { type: string }
        "404": { description: Não encontrado }
        "401": { description: Token inválido ou ausente }

  /events/{eventKey}/notes:
    parameters:
      - name: eventKey
        in: path
        required: true
        schema: { type: string }
        description: ID numérico ou `fake_id` do evento
    get:
      tags: [Notas de evento]
      operationId: listEventNotes
      summary: Listar notas/atas do evento
      description: |
        Lista notas do evento. Por padrão inclui rascunhos. Use `only_published=true`
        para só publicadas. Cada item inclui `public_url` (`/nota/<share_token>`) e anexos.
      parameters:
        - name: only_published
          in: query
          schema: { type: boolean, default: false }
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/EventNote" }
                  total: { type: integer }
                  event_id: { type: integer }
                  event_fake_id: { type: string }
        "404": { description: Evento não encontrado }
        "401": { description: Token inválido ou ausente }
    post:
      tags: [Notas de evento]
      operationId: createEventNote
      summary: Criar nota/ata no evento
      description: |
        Cria uma nota/ata vinculada ao evento.

        - Obrigatório: `title`.
        - `access_type`: `public` (qualquer visitante), `deps` (grupos), `users` (membros), `private` (só equipe — default).
        - `status`: `draft` ou `published` (default `published`).
        - `body_html` aceita HTML (Summernote).
        - `file_ids` promove anexos órfãos já enviados pelo painel (`event_note_upload_file`).
        - Gera `share_token` e `public_url` automaticamente.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [title]
              properties:
                title: { type: string, example: "Ata da reunião de kickoff" }
                summary: { type: string, maxLength: 500 }
                body_html: { type: string, description: "HTML do corpo" }
                meeting_at: { type: string, example: "2026-09-15 14:00:00" }
                access_type: { type: string, enum: [public, deps, users, private], default: private }
                access_deps: { type: string, description: "CSV de IDs de companies_dep (quando access_type=deps)" }
                access_users: { type: string, description: "CSV de user_ids (quando access_type=users)" }
                status: { type: string, enum: [draft, published], default: published }
                file_ids:
                  type: array
                  items: { type: integer }
                  description: IDs de anexos órfãos já enviados (upload do painel)
            example:
              title: Ata da reunião
              summary: Resumo curto
              body_html: "<p>Conteúdo da ata.</p>"
              access_type: private
              status: published
      responses:
        "201":
          description: Criada
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok: { type: boolean }
                  data: { $ref: "#/components/schemas/EventNote" }
        "404": { description: Evento não encontrado }
        "422": { description: Validação (title obrigatório) }
        "401": { description: Token inválido ou ausente }

  /events/{eventKey}/notes/{noteKey}:
    parameters:
      - name: eventKey
        in: path
        required: true
        schema: { type: string }
        description: ID numérico ou `fake_id` do evento
      - name: noteKey
        in: path
        required: true
        schema: { type: string }
        description: ID numérico da nota; no GET também aceita `share_token`
    get:
      tags: [Notas de evento]
      operationId: getEventNote
      summary: Obter nota/ata
      description: Resolve por id numérico ou `share_token` (GET). Inclui anexos e `public_url`.
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/EventNote" }
        "404": { description: Não encontrado }
        "401": { description: Token inválido ou ausente }
    patch:
      tags: [Notas de evento]
      operationId: updateEventNote
      summary: Atualizar nota/ata (parcial)
      description: Atualização parcial. Mesmos campos do POST (envie só o que mudar).
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                title: { type: string }
                summary: { type: string }
                body_html: { type: string }
                meeting_at: { type: string }
                access_type: { type: string, enum: [public, deps, users, private] }
                access_deps: { type: string }
                access_users: { type: string }
                status: { type: string, enum: [draft, published] }
                file_ids: { type: array, items: { type: integer } }
            example:
              title: Ata da reunião (editada)
              status: published
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok: { type: boolean }
                  data: { $ref: "#/components/schemas/EventNote" }
        "404": { description: Não encontrado }
        "422": { description: Validação }
        "401": { description: Token inválido ou ausente }
    delete:
      tags: [Notas de evento]
      operationId: deleteEventNote
      summary: Remover nota/ata (soft-delete)
      description: Soft-delete (`event_notes.deleted_at`). A URL pública `/nota/<token>` passa a responder 404.
      responses:
        "200":
          description: Removida
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok: { type: boolean, example: true }
                  id: { type: integer }
                  message: { type: string }
        "404": { description: Não encontrado }
        "401": { description: Token inválido ou ausente }

  /public/ai-results:
    get:
      summary: Listar os últimos resultados gerados pela Otto IA (SEM Bearer)
      description: |
        Endpoint **aberto** — não requer Bearer token. Lista os últimos (até 500)
        resultados gerados pelas IAs do sistema (sites, blog, posts de redes
        sociais, banners, templates de e-mail e imagens avulsas), de qualquer
        empresa, com paginação. Ordenado do mais recente para o mais antigo.

        Por razões de segurança/privacidade desta rota pública, o payload
        **não** expõe `company_id` nem `user_id`. Os textos são limpos de HTML
        e truncados em 1500 caracteres.

        **Rate limit por IP**: ~10 req/s (mais baixo que o autenticado).
        Respostas incluem `X-RateLimit-Remaining`; `429` quando excedido.
      security: []
      parameters:
        - $ref: "#/components/parameters/page"
        - $ref: "#/components/parameters/perPage"
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      type: object
                      properties:
                        result_type: { type: string, nullable: true, enum: [site_url, blog, social_post, banner, template_email, random_image] }
                        result_url: { type: string, nullable: true, example: "https://app.4.works/sites/meu-site" }
                        result_body: { type: string, nullable: true, description: "HTML removido, máx. 1500 chars." }
                        created_at: { type: string, nullable: true, example: "2026-05-29 11:40:00" }
                        created_at_iso: { type: string, nullable: true, example: "2026-05-29T11:40:00-03:00" }
                  meta:
                    type: object
                    properties:
                      total: { type: integer }
                      page: { type: integer }
                      per_page: { type: integer }
                      total_pages: { type: integer }
        "429":
          description: Rate limit excedido (10 req/s por IP). Veja header `Retry-After`.
  /nutrir/leads-lists:
    get:
      summary: Listar listas Nutrir
      parameters:
        - $ref: "#/components/parameters/page"
        - $ref: "#/components/parameters/perPage"
      responses:
        "200": { description: OK }
    post:
      summary: Criar lista
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name]
              properties:
                name: { type: string }
      responses:
        "201": { description: Criado }
  /nutrir/leads-lists/{id}:
    parameters:
      - name: id
        in: path
        required: true
        schema: { type: integer }
    get:
      summary: Obter lista por id
      responses:
        "200": { description: OK }
        "404": { description: Não encontrado }
    patch:
      summary: Atualizar nome da lista
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                name: { type: string }
      responses:
        "200": { description: OK }
    delete:
      summary: Remover lista (e leads associados; lista padrão do site não pode ser removida)
      responses:
        "204": { description: Sem corpo }
        "422": { description: Regra de negócio }
  /nutrir/leads:
    get:
      summary: Listar leads de uma lista
      parameters:
        - name: list_id
          in: query
          required: true
          schema: { type: integer }
        - $ref: "#/components/parameters/page"
        - $ref: "#/components/parameters/perPage"
        - name: q
          in: query
          schema: { type: string }
        - name: sort_by
          in: query
          schema: { type: string, enum: [first_name, email, phone, points, created_at] }
        - name: sort_dir
          in: query
          schema: { type: string, enum: [asc, desc] }
      responses:
        "200": { description: OK }
    post:
      summary: Criar lead
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                list_id: { type: integer }
                first_name: { type: string }
                lastname: { type: string }
                email: { type: string }
                phone: { type: string }
                extra_json: { type: string, description: JSON string ou objeto serializado }
                log_text: { type: string }
      responses:
        "201": { description: Criado }
  /nutrir/leads/{id}:
    parameters:
      - name: id
        in: path
        required: true
        schema: { type: integer }
    get:
      summary: Obter lead
      responses:
        "200": { description: OK }
    patch:
      summary: Atualizar lead
      responses:
        "200": { description: OK }
    delete:
      summary: Soft-delete do lead
      responses:
        "204": { description: Sem corpo }
  /crm/tags:
    get:
      summary: Listar tags
      parameters:
        - $ref: "#/components/parameters/page"
        - $ref: "#/components/parameters/perPage"
        - name: q
          in: query
          schema: { type: string }
      responses:
        "200": { description: OK }
    post:
      summary: Criar tag
      requestBody:
        content:
          application/json:
            schema:
              type: object
              required: [tag_name]
              properties:
                tag_name: { type: string }
      responses:
        "201": { description: Criado }
  /crm/tags/{id}:
    parameters:
      - name: id
        in: path
        required: true
        schema: { type: integer }
    get:
      summary: Obter tag
      responses:
        "200": { description: OK }
    patch:
      summary: Atualizar tag
      responses:
        "200": { description: OK }
    delete:
      summary: Remover tag (e relações user_tags)
      responses:
        "204": { description: Sem corpo }
  /crm/deal-menus:
    get:
      summary: Listar menus do tipo funil (deals) — somente leitura
      parameters:
        - $ref: "#/components/parameters/page"
        - $ref: "#/components/parameters/perPage"
      responses:
        "200": { description: OK }
  /crm/menus/{menu_id}/deal-steps:
    parameters:
      - name: menu_id
        in: path
        required: true
        schema: { type: integer }
    get:
      summary: Listar etapas do funil
      parameters:
        - $ref: "#/components/parameters/page"
        - $ref: "#/components/parameters/perPage"
      responses:
        "200": { description: OK }
    post:
      summary: Criar etapa
      requestBody:
        content:
          application/json:
            schema:
              type: object
              required: [name]
              properties:
                name: { type: string }
                step_order: { type: integer }
      responses:
        "201": { description: Criado }
  /crm/deal-steps/{id}:
    parameters:
      - name: id
        in: path
        required: true
        schema: { type: integer }
    get:
      summary: Obter etapa
      responses:
        "200": { description: OK }
    patch:
      summary: Atualizar etapa
      responses:
        "200": { description: OK }
    delete:
      summary: Remover etapa (bloqueado se houver negociações)
      responses:
        "204": { description: Sem corpo }
  /crm/menus/{menu_id}/deals:
    parameters:
      - name: menu_id
        in: path
        required: true
        schema: { type: integer }
    get:
      summary: Listar negociações do funil
      parameters:
        - $ref: "#/components/parameters/page"
        - $ref: "#/components/parameters/perPage"
        - name: step_id
          in: query
          schema: { type: integer }
        - name: status
          in: query
          schema: { type: string }
        - name: q
          in: query
          schema: { type: string }
      responses:
        "200": { description: OK }
    post:
      summary: Criar negociação
      requestBody:
        content:
          application/json:
            schema:
              type: object
              required: [name, step_id]
              properties:
                name: { type: string }
                step_id: { type: integer }
                responsible_user_id: { type: integer, description: "Se omitido, usa primeiro admin da empresa" }
                temperature: { type: string }
                total_value: { type: number }
                expected_close_date: { type: string, format: date }
                source: { type: string }
                campaign: { type: string }
                company_relation_id: { type: integer }
                description: { type: string }
      responses:
        "201": { description: Criado }
  /crm/deals/{id}:
    parameters:
      - name: id
        in: path
        required: true
        schema: { type: integer }
    get:
      summary: Obter negociação
      responses:
        "200": { description: OK }
    patch:
      summary: Atualizar campos permitidos da negociação
      responses:
        "200": { description: OK }
    delete:
      summary: Soft-delete (deleted_at)
      responses:
        "204": { description: Sem corpo }
  /crm/menus/{menu_id}/startups:
    parameters:
      - name: menu_id
        in: path
        required: true
        schema: { type: integer }
    get:
      summary: Listar empresas/oportunidades (startups) do funil
      parameters:
        - $ref: "#/components/parameters/page"
        - $ref: "#/components/parameters/perPage"
        - name: q
          in: query
          schema: { type: string }
      responses:
        "200": { description: OK }
    post:
      summary: Criar startup
      requestBody:
        content:
          application/json:
            schema:
              type: object
              required: [title]
              properties:
                title: { type: string }
                descri: { type: string }
                logo: { type: string }
                approved: { type: integer }
                members: { type: string }
                user_id: { type: integer, description: "Dono; default primeiro admin" }
      responses:
        "201": { description: Criado }
  /crm/startups/{id}:
    parameters:
      - name: id
        in: path
        required: true
        schema: { type: integer }
    get:
      summary: Obter startup
      responses:
        "200": { description: OK }
    patch:
      summary: Atualizar startup
      responses:
        "200": { description: OK }
    delete:
      summary: Soft-delete (removed_at)
      responses:
        "204": { description: Sem corpo }
  /crm/deals-contacts:
    get:
      summary: Listar contatos CRM de uma startup
      parameters:
        - name: company_relation_id
          in: query
          required: true
          schema: { type: integer }
        - $ref: "#/components/parameters/page"
        - $ref: "#/components/parameters/perPage"
      responses:
        "200": { description: OK }
    post:
      summary: Criar contato
      requestBody:
        content:
          application/json:
            schema:
              type: object
              required: [company_relation_id, name]
              properties:
                company_relation_id: { type: integer }
                name: { type: string }
                email: { type: string }
                whatsapp: { type: string }
                cargo: { type: string }
                observacoes: { type: string }
      responses:
        "201": { description: Criado }
  /crm/deals-contacts/{id}:
    parameters:
      - name: id
        in: path
        required: true
        schema: { type: integer }
    get:
      summary: Obter contato
      responses:
        "200": { description: OK }
    patch:
      summary: Atualizar contato
      responses:
        "200": { description: OK }
    delete:
      summary: Soft-delete
      responses:
        "204": { description: Sem corpo }
  /atrair/blog-articles:
    get:
      summary: Listar artigos do blog (Atrair)
      parameters:
        - $ref: "#/components/parameters/page"
        - $ref: "#/components/parameters/perPage"
        - name: status
          in: query
          schema: { type: string, enum: [draft, published, removed] }
      responses:
        "200": { description: OK }
    post:
      summary: Criar artigo
      requestBody:
        content:
          application/json:
            schema:
              type: object
              required: [title]
              properties:
                title: { type: string }
                body: { type: string }
                cover: { type: string }
                hashtags: { type: string }
                status: { type: string }
                author_id: { type: integer }
                ai_generated: { type: integer }
      responses:
        "201": { description: Criado }
  /atrair/blog-articles/{id}:
    parameters:
      - name: id
        in: path
        required: true
        schema: { type: integer }
    get:
      summary: Obter artigo
      responses:
        "200": { description: OK }
    patch:
      summary: Atualizar artigo
      responses:
        "200": { description: OK }
    delete:
      summary: Marcar como removed (soft)
      responses:
        "204": { description: Sem corpo }
  /atrair/blog-articles/ai-generate:
    post:
      summary: Gerar artigo de blog com a Otto IA (texto + capa)
      description: >
        Gera um artigo de blog usando IA (GPT para o texto + imagem de capa opcional).
        O artigo é criado como rascunho (draft). Consome a cota de tokens de IA compartilhada
        (site_ai_sites.tokens_used). Retorna 402 quando a cota do plano é excedida.
      requestBody:
        content:
          application/json:
            schema:
              type: object
              required: [topic]
              properties:
                topic:
                  type: string
                  description: Tema/assunto do artigo. Aceita os aliases "theme" e "prompt".
                qty:
                  type: integer
                  minimum: 1
                  maximum: 5
                  default: 1
                  description: Quantidade de artigos a gerar (máx. 5 por chamada).
                size:
                  type: integer
                  minimum: 150
                  maximum: 2000
                  default: 500
                  description: Tamanho aproximado em palavras.
                with_image:
                  type: boolean
                  default: true
                  description: Gerar imagem de capa. Aceita o alias "with_cover".
      responses:
        "200": { description: Artigos gerados }
        "402": { description: Cota de tokens de IA excedida }
        "422": { description: Validação }
  /ai/sites:
    get:
      summary: Informações do site gerado pela Otto IA
      description: Retorna slug, estado de publicação e URL pública/privada do site da empresa.
      responses:
        "200": { description: OK }
    post:
      summary: Iniciar geração de site completo com a Otto IA (assíncrono)
      description: >
        Inicia a geração de um site completo a partir de um prompt (modelo Anthropic
        Claude do Construtor de Sites). A geração é PESADA (4–7 min), então em
        produção/staging roda em segundo plano: este endpoint responde **202** com
        `status:"processing"` e um `job_id`. Consulte `GET /ai/sites/status?job_id=N`
        a cada ~15s até `status:"done"` — nesse momento o site já foi publicado (slug)
        e a `url` pública é retornada. Em ambiente local a geração roda síncrona e já
        devolve `200` com a URL pronta. Consome a cota de tokens de IA compartilhada.
      requestBody:
        content:
          application/json:
            schema:
              type: object
              required: [prompt]
              properties:
                prompt:
                  type: string
                  description: Descrição do site (empresa, oferta, seções, estilo). Alias "story".
                offer:
                  type: string
                  description: Produto/serviço/oferta principal (opcional).
                menus:
                  type: string
                  description: Estrutura de menus desejada (opcional).
                colors:
                  type: array
                  items: { type: string }
                  description: Paleta de cores em HEX (opcional).
                logo_url:
                  type: string
                  description: URL da logomarca (opcional).
                publish:
                  type: boolean
                  default: true
                  description: Publicar automaticamente ao concluir a geração.
                slug:
                  type: string
                  description: Slug desejado para a URL pública (opcional).
      responses:
        "202":
          description: >
            Geração iniciada em segundo plano. Devolve `{ status:"processing", job_id,
            poll_url, poll_after_seconds, estimated_seconds }`. Consulte
            `GET /ai/sites/status?job_id=N` periodicamente.
        "200": { description: (Somente em ambiente local) Site gerado síncrono, com URL pronta }
        "402": { description: Cota de tokens de IA excedida }
        "422": { description: Validação }
  /ai/sites/status:
    get:
      summary: Consultar status da geração assíncrona do site
      description: >
        Verifica o andamento de um job de geração iniciado por `POST /ai/sites`.
        Enquanto `status:"processing"`, repita a cada ~15s. Quando `status:"done"`,
        o site já foi publicado e o payload traz `url` (URL pública), `slug`,
        `used_tokens` e `limit_tokens`. Quando `status:"failed"`, traz `message`.
      parameters:
        - in: query
          name: job_id
          required: true
          schema: { type: integer }
          description: ID do job retornado por POST /ai/sites.
      responses:
        "200":
          description: >
            Status do job. `status` ∈ (processing | done | failed). Em `done`, inclui
            `url`, `slug`, `published` e contadores de tokens.
        "404": { description: Job não encontrado para esta empresa }
        "422": { description: Validação (job_id ausente) }
  /ai/sites/publish:
    post:
      summary: Publicar o site gerado pela Otto IA
      description: >
        Ativa o site gerado, deixando-o acessível na URL pública. Se o corpo trouxer
        `slug`, ele é usado (validando unicidade); caso contrário reaproveita o slug
        atual ou resolve um a partir do nome da comunidade. Retorna o site com
        `published:true`, `slug` e `url`.
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                slug:
                  type: string
                  description: Slug desejado para a URL pública (opcional, mín. 3 caracteres).
      responses:
        "200": { description: Site publicado }
        "404": { description: Nenhum site gerado para esta empresa }
        "409": { description: Slug já em uso por outro site }
        "422": { description: Slug inválido (mín. 3 caracteres) }
  /ai/sites/unpublish:
    post:
      summary: Despublicar o site gerado pela Otto IA
      description: >
        Desativa o site (`activated=0`). O conteúdo e o slug são mantidos; o site
        fica acessível apenas pelo link privado (`?k=<access_token>`). Retorna o site
        com `published:false`.
      responses:
        "200": { description: Site despublicado }
        "404": { description: Nenhum site gerado para esta empresa }
  /ai/images:
    get:
      summary: Cota de tokens de IA (imagens)
      description: Retorna tokens usados/limite/restante para geração de imagens.
      responses:
        "200": { description: OK }
    post:
      summary: Gerar imagem com a Otto IA
      description: >
        Gera uma imagem via gpt-image-2, hospeda no S3 e retorna a URL pública.
        Consome a cota de tokens de IA compartilhada.
      requestBody:
        content:
          application/json:
            schema:
              type: object
              required: [prompt]
              properties:
                prompt: { type: string }
                size:
                  type: string
                  enum: ["1024x1024", "1024x1536", "1536x1024", "auto"]
                  default: "1536x1024"
                quality:
                  type: string
                  enum: [low, medium, high, auto]
                  default: high
      responses:
        "200": { description: Imagem gerada, com URL pública }
        "402": { description: Cota de tokens de IA excedida }
        "422": { description: Validação }
components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: companies.api_key | companies.api_token
  parameters:
    page:
      name: page
      in: query
      schema: { type: integer, minimum: 1, default: 1 }
    perPage:
      name: per_page
      in: query
      schema: { type: integer, minimum: 1, maximum: 100, default: 30 }
  schemas:
    Event:
      type: object
      properties:
        id: { type: integer }
        fake_id: { type: string }
        title: { type: string }
        description: { type: string, nullable: true }
        cover_url: { type: string, nullable: true }
        start_at: { type: string, nullable: true }
        start_at_iso: { type: string, nullable: true }
        end_at: { type: string, nullable: true }
        end_at_iso: { type: string, nullable: true }
        place: { type: string, nullable: true }
        type: { type: string, nullable: true, enum: [on-line, presencial, hibrido] }
        status: { type: integer, enum: [0, 1, -1] }
        hidden: { type: boolean }
        need_confirmation: { type: boolean }
        event_color: { type: string, nullable: true }
        inscription_type: { type: string, nullable: true }
        event_inscription_link: { type: string, nullable: true }
        public_url: { type: string, nullable: true }
        dashboard_url: { type: string, nullable: true }
        tickets:
          type: array
          items: { type: object }
        ticket_primary: { type: object, nullable: true }
        created_at: { type: string, nullable: true }
    EventNote:
      type: object
      properties:
        id: { type: integer }
        event_id: { type: integer }
        share_token: { type: string }
        title: { type: string }
        summary: { type: string, nullable: true }
        body_html: { type: string, nullable: true }
        meeting_at: { type: string, nullable: true }
        access_type: { type: string, enum: [public, deps, users, private] }
        access_deps: { type: string, nullable: true }
        access_users: { type: string, nullable: true }
        status: { type: string, enum: [draft, published] }
        views: { type: integer }
        public_url: { type: string, nullable: true }
        files:
          type: array
          items:
            type: object
            properties:
              id: { type: integer }
              file_fake_id: { type: string }
              file_name: { type: string }
              file_kind: { type: string, enum: [audio, video, image, file] }
              download_url: { type: string }
              transcription_status: { type: string, nullable: true }
        created_at: { type: string, nullable: true }
        updated_at: { type: string, nullable: true }
