openapi: 3.1.0
info:
  title: LiteZap API
  version: "0.4.0"
  summary: API de WhatsApp não oficial, open source, que roda local, no seu servidor ou na nuvem.
  description: |
    Referência da API REST do motor LiteZap (até o marco 11): instâncias e pareamento, envio com fila
    persistente e visível, recebimento, chats, contatos, verificação de números, mídia, proxy por
    instância, grupos e comunidades, perfil e privacidade, status, canais, etiquetas, atendente IA,
    plataforma (chaves de API, links de pareamento, lote), webhooks assinados, eventos em tempo real por
    SSE e servidor MCP em `/mcp`. Os eventos entregues por webhook estão descritos na seção `webhooks`
    deste arquivo.

    Autenticação: `Authorization: Bearer <token>`. O token admin acessa tudo; o token de instância
    (prefixo `lzi_`) só acessa as rotas da própria instância; uma chave de API (prefixo `lzk_`, marco 11)
    acessa só as rotas dos seus escopos (`read`, `send`) numa instância. Tokens e chaves podem ter
    allowlist de IP.

    Erros seguem sempre o envelope `{"error":{"code","message","retryable"}}`.
  license:
    name: Apache-2.0
    identifier: Apache-2.0
servers:
  - url: http://127.0.0.1:8787
    description: Padrão local (listen_addr)
  - url: https://api.seu-dominio.com
    description: Self-hosted atrás de proxy reverso com TLS
security:
  - bearer: []
tags:
  - name: Instâncias
    description: Criar, listar, controlar e apagar instâncias (um número de WhatsApp cada).
    x-slug: instancias
    x-intro: |
      Uma instância é um número de WhatsApp conectado ao LiteZap, com token próprio, configurações de
      envio e um processo worker dedicado. Criar, listar, apagar e rotacionar token exigem o token admin;
      as demais ações aceitam o token da própria instância.

      As ações de ciclo de vida respondem logo depois de disparadas: acompanhe o estado com
      `GET /v1/instances/{id}` ou pelo evento `instance.status`. Os estados estão em
      [Instâncias e estados](/docs/conceitos/instancias/).
    x-concept: /docs/conceitos/instancias/
  - name: Pareamento
    description: Conectar o número por QR code ou código de pareamento por telefone.
    x-slug: pareamento
    x-intro: |
      Uma instância nova (ou deslogada) fica em `qr_pending` até ser pareada. Há dois caminhos: escanear o
      QR em WhatsApp > Aparelhos conectados, ou pedir um código e digitá-lo no celular. Os dois também
      chegam como eventos (`instance.qr` e `instance.pair_code`), úteis para mostrar o QR numa tela sua
      sem polling.
    x-concept: /docs/quickstart/
  - name: Configurações
    description: Ritmo, rampa, janela de envio, fuso e proxy de cada instância.
    x-slug: configuracoes
    x-intro: |
      Cada instância herda os `instance_defaults` da configuração do servidor e guarda só os campos que
      você alterar. As configurações controlam a [fila e o ritmo](/docs/conceitos/fila-e-ritmo/), a
      [rampa](/docs/conceitos/rampa/), a [janela de envio](/docs/conceitos/janela-de-envio/) e o
      [proxy](/docs/conceitos/proxy/). `proxy_url` e `unsafe_pacing` exigem o token admin.
    x-concept: /docs/conceitos/fila-e-ritmo/
  - name: Mensagens
    description: Envio pela fila persistente, consulta de status e controle da fila.
    x-slug: mensagens
    x-intro: |
      Todo envio entra numa fila persistente por instância e sai no ritmo configurado, respeitando a
      carência pós-conexão, a janela de envio, a rampa e a verificação de número. O envio responde `202`
      na hora, ou espera o resultado com `?wait=true`. Os campos de cada tipo estão em
      [Tipos de mensagem](/docs/api/mensagens/tipos/).
    x-concept: /docs/conceitos/fila-e-ritmo/
  - name: Chats
    description: Chats e histórico do log de mensagens, marcar como lido.
    x-slug: chats
    x-intro: |
      O LiteZap guarda um log local de tudo o que a instância envia e recebe, inclusive o histórico
      sincronizado no pareamento. Chats e histórico vêm desse log, com paginação por cursor, sem consultar
      o WhatsApp a cada chamada. Use o `jid` que aparece em `GET /chats` nas rotas `/chats/{jid}/...`.
    x-concept: /docs/conceitos/identificadores/
  - name: Contatos
    description: Contatos conhecidos pela sessão do WhatsApp.
    x-slug: contatos
    x-intro: |
      Contatos vêm da sessão do WhatsApp guardada pelo worker (agenda sincronizada e nomes de perfil). As
      rotas consultam o worker, então a instância precisa estar rodando.
    x-concept: /docs/conceitos/identificadores/
  - name: Números
    description: Verificar se números têm WhatsApp.
    x-slug: numeros
    x-intro: |
      Confere se números têm WhatsApp e devolve o JID que o WhatsApp usa (no Brasil, com ou sem o 9º
      dígito). A fila já faz essa verificação antes do primeiro envio a cada número; este endpoint serve
      para conferir contatos pontuais, com cache e cota diária.
    x-concept: /docs/conceitos/verificacao-de-numeros/
  - name: Grupos
    description: Criar e administrar grupos, participantes, convites e pedidos de entrada.
    x-slug: grupos
    x-intro: |
      Gestão dos grupos em que o número está (marco 8). As rotas são síncronas (até 30 segundos) e a
      instância precisa estar conectada. Operações com participantes devolvem um resultado por pessoa:
      uma falha nunca derruba o lote.

      Adicionar desconhecidos em grupos é um dos gatilhos de banimento mais rápidos. Por isso só são
      adicionadas pessoas que já conversam com o número ou que a consulta de números confirma no
      WhatsApp, no máximo 10 por requisição, com 10 segundos entre lotes e cotas por hora e por dia
      (`settings.groups`). Criar grupos e entrar por link também têm cota diária.
    x-concept: /docs/guias/grupos/
  - name: Comunidades
    description: Criar comunidades e vincular grupos a elas.
    x-slug: comunidades
    x-intro: |
      Uma comunidade é um grupo "pai" que reúne grupos; ao criá-la, o WhatsApp cria sozinho o grupo de
      avisos. Nome, descrição, configurações, link e participantes da comunidade usam as rotas de
      [Grupos](/docs/api/grupos/) com o JID dela.
    x-concept: /docs/guias/grupos/
  - name: Mídia
    description: Baixar mídia recebida e gerar URLs assinadas temporárias.
    x-slug: midia
    x-intro: |
      Mídia recebida é baixada sob demanda na primeira leitura e guardada em `data/media/`. Para repassar
      um arquivo a outro serviço sem entregar seu token, gere uma URL assinada que vale 1 hora.
    x-concept: /docs/conceitos/midia/
  - name: Atendente IA
    description: Hook de agente (externo ou embutido) e o Atendente LiteZap — agentes treinados com o contexto do negócio.
    x-slug: atendente-ia
    x-intro: |
      O hook de agente entrega cada mensagem recebida a um atendente de IA e envia a resposta pela fila normal
      (ritmo, janela, rampa, suspensão), com pausa quando um humano responde, proteção contra loop e um chat por
      vez. O destino é a sua URL (`target: url`, POST assinado) ou o **Atendente LiteZap** embutido
      (`target: builtin`): a IA roda dentro do motor, com agentes por instância (modo vendedor, facilitador de
      pedido, SDR/agendamento ou suporte), base de conhecimento, transcrição de áudio, imagens para o modelo e
      resposta em voz opcional. O provedor (Anthropic ou OpenAI-compatível) e as chaves são do admin; as chaves
      ficam cifradas e nunca voltam na API. O passo a passo está em [Atendente IA](/docs/guias/atendimento-ia/).
    x-concept: /docs/guias/atendimento-ia/
  - name: Webhooks
    description: Cadastrar endpoints que recebem eventos assinados, com retry e dead-letter.
    x-slug: webhooks
    x-intro: |
      Um webhook recebe por `POST` cada evento que combina com os filtros dele, assinado com HMAC-SHA256.
      Entregas ficam numa tabela (outbox), sobrevivem a restarts e são repetidas por até 24 horas; depois
      vão para a dead-letter, de onde podem ser reenviadas. Com token admin o webhook pode ser global
      (todas as instâncias) ou de uma instância; com token de instância é sempre da própria instância.

      Payloads de cada evento estão no [catálogo de eventos](/docs/eventos/) e a verificação da assinatura
      em [Verificar assinatura](/docs/eventos/assinatura/).
    x-concept: /docs/guias/webhooks/
  - name: Eventos em tempo real
    description: Streams Server-Sent Events por instância e global.
    x-slug: sse
    x-intro: |
      Os mesmos eventos dos webhooks, ao vivo por Server-Sent Events, sem precisar de um servidor público.
      Não há replay: eventos de quando o cliente estava desconectado não voltam (para isso, use webhooks).
    x-concept: /docs/guias/sse/
  - name: MCP
    description: Servidor Model Context Protocol para assistentes de IA.
    x-slug: mcp
    x-intro: |
      O LiteZap expõe as mesmas operações da API como ferramentas MCP, para assistentes de IA (Claude,
      ChatGPT e outros) lerem conversas e enviarem mensagens pelo seu número, sempre pela mesma fila e com as
      mesmas proteções. O endpoint fala o protocolo MCP (JSON-RPC sobre streamable HTTP), não REST: use um
      cliente MCP. A lista de ferramentas e a configuração dos clientes estão em [MCP e IA](/docs/mcp/).
    x-concept: /docs/mcp/
  - name: Perfil e privacidade
    description: Nome, recado e foto da conta, configurações de privacidade e bloqueios.
    x-slug: perfil-e-privacidade
    x-intro: |
      O perfil e a privacidade valem para a conta toda: o celular mostra o mesmo que a API. Nome e recado
      seguem os limites do WhatsApp (25 e 139 caracteres); a foto é recortada no centro e reduzida a
      640x640 (JPEG). Bloquear um contato vale também no celular.
    x-concept: /docs/conceitos/instancias/
  - name: Status
    description: Publicar status (stories), ver os dos contatos e responder.
    x-slug: status
    x-intro: |
      Status de texto, imagem ou vídeo (marco 10). Os destinatários são os da privacidade do status do
      celular: o WhatsApp não permite escolher por envio. Publicar passa pela fila de envio (ritmo,
      janela, rampa) e tem teto de 10 por dia por instância. Os status dos contatos ficam no log e saem
      no evento opt-in `status.received`.
    x-concept: /docs/conceitos/limites/
  - name: Canais
    description: Seguir, ler, reagir, criar e publicar em canais (newsletters).
    x-slug: canais
    x-intro: |
      Canais são listas públicas de mão única (marco 10). Seguir e deixar de seguir têm limite de 3 por
      minuto, 10 por hora e 30 por dia; criar, 2 por dia; reagir, 30 por minuto. Publicar só como dono ou
      admin, pela fila de envio, até 20 publicações por dia. Editar o canal, apagar, listar inscritos e buscar no diretório não existem no
      whatsmeow.
    x-concept: /docs/conceitos/limites/
  - name: Etiquetas e Business
    description: Etiquetas e perfil comercial de contas WhatsApp Business.
    x-slug: business
    x-intro: |
      Só contas WhatsApp Business (em conta pessoal, `409 business_account_required`). As etiquetas vêm do
      celular pelo app state e cada mudança sai em `label.updated`. O perfil comercial é só leitura;
      catálogo, coleções e pedidos não existem no whatsmeow.
    x-concept: /docs/conceitos/instancias/
  - name: Operação
    description: Saúde do processo, prontidão e identidade do token.
    x-slug: operacao
    x-intro: |
      Rotas para monitoramento e orquestradores. São públicas quando o servidor escuta em loopback e
      exigem o token admin em qualquer outro endereço.
    x-concept: /docs/self-hosted/observabilidade/
  - name: Plataforma
    description: Recursos para parceiros e multi-instância — lote, chaves de API com escopo, links de pareamento e uso.
    x-slug: plataforma
    x-intro: |
      Rotas para quem oferece WhatsApp a clientes (SaaS, revendas, agências) — marco 11. Etiquete as
      instâncias para filtrá-las e operar em lote, crie chaves de API só de leitura ou só de envio para
      entregar a terceiros, gere um link de pareamento hospedado (o cliente escaneia o QR numa página do
      seu servidor, sem ver token nenhum) e consulte o uso de cada instância.

      Renomear, etiquetar e restringir IPs fica em [Editar instância](/docs/api/instancias/editar-instancia/);
      filtros e paginação em [Listar instâncias](/docs/api/instancias/listar-instancias/). O passo a passo
      está no guia [Multi-instância e parceiros](/docs/guias/multi-instancia/).
    x-concept: /docs/guias/multi-instancia/

paths:
  /v1/instances:
    post:
      tags: [Instâncias]
      operationId: createInstance
      x-litezap-milestone: M1
      x-zapi: "Criar instância (parceiros)"
      x-litezap-events: [instance.status]
      summary: Criar instância
      x-litezap-scope: admin
      description: |
        Cria a instância e já sobe o worker dela (o estado passa de `created` a `starting` e depois
        `qr_pending`). O token da instância aparece **uma única vez** nesta resposta; o servidor guarda
        só o hash. A resposta leva `Cache-Control: no-store`.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name]
              properties:
                name: { type: string, description: Nome livre para identificar a instância. }
            example: { name: "atendimento" }
      responses:
        "201":
          description: Instância criada.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/InstanceCreated" }
              example:
                instance:
                  id: inst_7f3a9c2e5b1d4086
                  name: atendimento
                  state: starting
                  created_at: "2026-10-17T13:45:00Z"
                  updated_at: "2026-10-17T13:45:00Z"
                token: lzi_Q2hhdmUtZGUtZXhlbXBsby1uYW8tdXNl
        "400": { $ref: "#/components/responses/ValidationFailed" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
    get:
      tags: [Instâncias]
      operationId: listInstances
      x-litezap-milestone: M1
      x-zapi: "Listar instâncias (parceiros)"
      summary: Listar instâncias
      x-litezap-scope: admin
      description: |
        Lista as instâncias em ordem de criação. Sem `limit`, devolve todas (como antes do marco 11). Com
        `limit`, pagina: siga `next_cursor` até ele não vir mais. Os filtros (marco 11) se combinam.
      parameters:
        - { name: state, in: query, description: "Um ou mais estados separados por vírgula (`connected,qr_pending`).", schema: { type: string } }
        - { name: tag, in: query, description: "Só instâncias com esta etiqueta.", schema: { type: string } }
        - { name: q, in: query, description: "Trecho do nome ou do id (sem diferenciar maiúsculas).", schema: { type: string } }
        - { name: limit, in: query, description: "Itens por página (1 a 500). Omitido: todas, sem paginação.", schema: { type: integer, minimum: 1, maximum: 500 } }
        - { $ref: "#/components/parameters/Cursor" }
      responses:
        "200":
          description: As instâncias (uma página, com `limit`).
          content:
            application/json:
              schema:
                type: object
                properties:
                  instances:
                    type: array
                    items: { $ref: "#/components/schemas/Instance" }
                  next_cursor: { type: string, description: "Presente quando há próxima página (só com `limit`)." }
              example:
                instances:
                  - id: inst_7f3a9c2e5b1d4086
                    name: atendimento
                    state: connected
                    phone: "5511999999999"
                    tags: [vip]
                    ip_allowlist: []
                    created_at: "2026-10-17T13:45:00Z"
                    updated_at: "2026-10-17T13:47:12Z"
                next_cursor: MjAyNi0xMC0xN1QxMzo0NTowMC4wMDAwMDAwMDBafGluc3RfN2YzYTljMmU1YjFkNDA4Ng
        "400": { $ref: "#/components/responses/ValidationFailed" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }

  /v1/instances/{id}:
    parameters:
      - $ref: "#/components/parameters/InstanceID"
    get:
      tags: [Instâncias]
      operationId: getInstance
      x-litezap-milestone: M1
      x-zapi: "Status da instância / Dados da instância"
      summary: Consultar instância
      x-litezap-scope: instancia
      description: Estado atual e número conectado. Os estados mudam de forma assíncrona; use esta rota para acompanhar (polling) até haver eventos (marco 3).
      responses:
        "200":
          description: A instância.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Instance" }
              example:
                id: inst_7f3a9c2e5b1d4086
                name: atendimento
                state: connected
                phone: "5511999999999"
                created_at: "2026-10-17T13:45:00Z"
                updated_at: "2026-10-17T13:47:12Z"
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/InstanceNotFound" }
    patch:
      tags: [Instâncias]
      operationId: updateInstance
      x-litezap-milestone: M11
      x-zapi: "Renomear instância"
      summary: Editar instância
      x-litezap-scope: instancia
      description: |
        Renomeia a instância e define etiquetas e a allowlist de IP do token dela. Só os campos enviados
        mudam. `name` aceita o token da própria instância; `tags` e `ip_allowlist` exigem o token admin.

        - `tags`: até 20, cada uma com 1 a 40 caracteres (letras minúsculas, dígitos e `_ . : -`); são
          guardadas em minúsculas e sem repetição. Servem para filtrar `GET /instances` e para operações em lote.
        - `ip_allowlist`: IPs ou CIDRs (até 100). Vazio (`[]`) libera qualquer endereço. Fora da lista, o
          token da instância recebe `403 forbidden` em todas as rotas (inclusive `/mcp`). Atrás de proxy
          reverso, configure `trusted_proxies` no servidor, senão o IP visto é o do proxy.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                name: { type: string, maxLength: 100 }
                tags: { type: array, items: { type: string } }
                ip_allowlist: { type: array, items: { type: string }, description: "IPs ou CIDRs." }
            example: { name: "Loja Centro", tags: [vip, sp], ip_allowlist: ["203.0.113.7", "10.0.0.0/8"] }
      responses:
        "200":
          description: A instância atualizada.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Instance" }
              example:
                id: inst_7f3a9c2e5b1d4086
                name: Loja Centro
                state: connected
                phone: "5511999999999"
                tags: [vip, sp]
                ip_allowlist: ["203.0.113.7/32", "10.0.0.0/8"]
                created_at: "2026-10-17T13:45:00Z"
                updated_at: "2026-10-18T09:00:00Z"
        "400": { $ref: "#/components/responses/ValidationFailed" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/InstanceNotFound" }
    delete:
      tags: [Instâncias]
      operationId: deleteInstance
      x-litezap-milestone: M1
      x-zapi: "Apagar instância (parceiros)"
      x-litezap-events: [instance.status]
      x-litezap-notes: "Apaga sessão, mídia, fila e histórico da instância. Não há como desfazer."
      summary: Apagar instância
      x-litezap-scope: admin
      description: |
        Faz logout (melhor esforço, até 15s, libera o aparelho no celular), para o worker e apaga a sessão,
        a mídia e a fila da instância. É a única saída para uma instância `banned`. Pode levar ~20s.
      responses:
        "204": { description: Apagada. }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/InstanceNotFound" }

  /v1/instances/{id}/start:
    parameters: [{ $ref: "#/components/parameters/InstanceID" }]
    post:
      tags: [Instâncias]
      operationId: startInstance
      x-litezap-milestone: M1
      x-zapi: "Reiniciar"
      x-litezap-events: [instance.status]
      summary: Iniciar worker
      x-litezap-scope: instancia
      description: Sobe o worker de uma instância `stopped`, `failed` ou `logged_out` (nesse caso volta a `qr_pending`). Responde assim que o worker é disparado; a instância devolvida pode ainda mostrar o estado anterior.
      responses:
        "200": { $ref: "#/components/responses/InstanceOK" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/InstanceNotFound" }
        "409": { $ref: "#/components/responses/InstanceConflict" }
        "503": { $ref: "#/components/responses/ShuttingDown" }
  /v1/instances/{id}/stop:
    parameters: [{ $ref: "#/components/parameters/InstanceID" }]
    post:
      tags: [Instâncias]
      operationId: stopInstance
      x-litezap-milestone: M1
      x-zapi: "(sem equivalente)"
      x-litezap-events: [instance.status]
      summary: Parar worker
      x-litezap-scope: instancia
      description: Para o worker e leva a instância a `stopped` (a sessão fica salva; `start` reconecta sem QR). Uma instância parada não sobe sozinha no próximo boot.
      responses:
        "200": { $ref: "#/components/responses/InstanceOK" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/InstanceNotFound" }
  /v1/instances/{id}/restart:
    parameters: [{ $ref: "#/components/parameters/InstanceID" }]
    post:
      tags: [Instâncias]
      operationId: restartInstance
      x-litezap-milestone: M1
      x-zapi: "Reiniciar"
      x-litezap-events: [instance.status]
      summary: Reiniciar worker
      x-litezap-scope: instancia
      description: Para e sobe o worker de novo. Responde assim que o novo worker é disparado.
      responses:
        "200": { $ref: "#/components/responses/InstanceOK" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/InstanceNotFound" }
        "409": { $ref: "#/components/responses/InstanceConflict" }
        "503": { $ref: "#/components/responses/ShuttingDown" }
  /v1/instances/{id}/logout:
    parameters: [{ $ref: "#/components/parameters/InstanceID" }]
    post:
      tags: [Instâncias]
      operationId: logoutInstance
      x-litezap-milestone: M1
      x-zapi: "Desconectar"
      x-litezap-events: [instance.status]
      x-litezap-notes: "Depois do logout o número precisa ser pareado de novo."
      summary: Desconectar número
      x-litezap-scope: instancia
      description: Desloga a sessão do WhatsApp (o aparelho some da lista no celular) e leva a instância a `logged_out`. Para usar de novo, `start` e parear outra vez.
      responses:
        "200": { $ref: "#/components/responses/InstanceOK" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/InstanceNotFound" }
        "409": { $ref: "#/components/responses/InstanceConflict" }
        "502": { $ref: "#/components/responses/WorkerError" }
        "504": { $ref: "#/components/responses/WorkerTimeout" }
  /v1/instances/{id}/token/rotate:
    parameters: [{ $ref: "#/components/parameters/InstanceID" }]
    post:
      tags: [Instâncias]
      operationId: rotateToken
      x-litezap-milestone: M1
      x-zapi: "(sem equivalente)"
      x-litezap-notes: "O token antigo deixa de valer na hora: atualize suas integrações antes."
      summary: Rotacionar token da instância
      x-litezap-scope: admin
      description: Gera um novo token de instância; o anterior deixa de valer na hora. O novo token aparece só nesta resposta.
      responses:
        "200":
          description: Novo token.
          content:
            application/json:
              schema:
                type: object
                properties:
                  token: { type: string }
              example: { token: lzi_Tm92by10b2tlbi1kZS1leGVtcGxv }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/InstanceNotFound" }

  /v1/instances/{id}/qr:
    parameters: [{ $ref: "#/components/parameters/InstanceID" }]
    get:
      tags: [Pareamento]
      operationId: getQR
      x-litezap-sample: { query: { format: png }, binary: qr.png }
      x-litezap-milestone: M1
      x-zapi: "Pegar QRCode / QRCode imagem"
      x-litezap-events: [instance.qr]
      summary: Obter QR code
      x-litezap-scope: instancia
      description: |
        QR atual para escanear em WhatsApp > Aparelhos conectados. O código muda a cada poucos segundos:
        busque de novo se expirar. Só responde enquanto a instância está `qr_pending`.
      parameters:
        - name: format
          in: query
          required: false
          description: "`png` devolve a imagem (320×320); omitido devolve o texto do QR em JSON."
          schema: { type: string, enum: [png] }
      responses:
        "200":
          description: Código do QR (JSON) ou imagem PNG.
          content:
            application/json:
              schema:
                type: object
                properties:
                  code: { type: string }
              example: { code: "2@kQ3x...,Zm9v...,YmFy...,MQ==" }
            image/png:
              schema: { type: string, format: binary }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/InstanceNotFound" }
        "409": { $ref: "#/components/responses/NotPairing" }
  /v1/instances/{id}/pair-phone:
    parameters: [{ $ref: "#/components/parameters/InstanceID" }]
    post:
      tags: [Pareamento]
      operationId: pairPhone
      x-litezap-milestone: M1
      x-zapi: "Conectar com telefone"
      x-litezap-events: [instance.pair_code]
      summary: Parear por código
      x-litezap-scope: instancia
      description: |
        Alternativa ao QR: gera um código para digitar no celular (Aparelhos conectados > Conectar com
        número de telefone). Só vale enquanto a instância está `qr_pending`.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [phone]
              properties:
                phone: { type: string, description: "Número do aparelho com DDI e DDD (10 a 15 dígitos)." }
            example: { phone: "5511999999999" }
      responses:
        "200":
          description: Código de pareamento.
          content:
            application/json:
              schema:
                type: object
                properties:
                  code: { type: string }
              example: { code: "ABCD-EFGH" }
        "400": { $ref: "#/components/responses/ValidationFailed" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/InstanceNotFound" }
        "409": { $ref: "#/components/responses/NotPairing" }
        "502": { $ref: "#/components/responses/WorkerError" }
        "504": { $ref: "#/components/responses/WorkerTimeout" }

  /v1/instances/{id}/settings:
    parameters: [{ $ref: "#/components/parameters/InstanceID" }]
    get:
      tags: [Configurações]
      operationId: getSettings
      x-litezap-milestone: M2
      x-zapi: "Configurações da fila"
      summary: Ler configurações
      x-litezap-scope: instancia
      description: Configurações efetivas (padrões da config + overrides da instância). A senha do `proxy_url` vem mascarada.
      responses:
        "200":
          description: Configurações efetivas.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Settings" }
              example:
                min_interval_ms: 3000
                jitter_ms: 2000
                max_per_minute: 20
                typing_simulation: true
                ramp: { enabled: true, start_per_day: 50, end_per_day: 150, days: 14 }
                send_window: { start: "08:00", end: "20:00" }
                timezone: ""
                unsafe_pacing: false
                proxy_url: ""
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/InstanceNotFound" }
    put:
      tags: [Configurações]
      operationId: updateSettings
      x-litezap-milestone: M2
      x-zapi: "Atualizar configurações da fila / Configurar proxy"
      x-litezap-notes: "Baixar os pisos de ritmo com unsafe_pacing ou desligar rampa e janela aumenta o risco de banimento."
      summary: Alterar configurações
      x-litezap-scope: instancia
      description: |
        Atualização parcial: campos ausentes mantêm o valor atual. `proxy_url` e `unsafe_pacing` exigem o
        token admin (`403 forbidden` com token de instância). Pisos: `min_interval_ms` ≥ 1000 e
        `max_per_minute` ≤ 60, salvo `unsafe_pacing`. `send_window` com `start` e `end` vazios libera
        qualquer horário. `"proxy_url": ""` volta ao `default_proxy_url`. Trocar o proxy reinicia o worker.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/SettingsPatch" }
            example:
              min_interval_ms: 5000
              ramp: { enabled: true }
              send_window: { start: "09:00", end: "18:00" }
              timezone: America/Sao_Paulo
      responses:
        "200":
          description: Configurações efetivas depois da alteração.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Settings" }
        "400": { $ref: "#/components/responses/ValidationFailed" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/InstanceNotFound" }

  /v1/instances/{id}/messages:
    parameters: [{ $ref: "#/components/parameters/InstanceID" }]
    post:
      tags: [Mensagens]
      operationId: sendMessage
      x-litezap-sample: { query: { wait: "true" } }
      x-litezap-milestone: M2
      x-zapi: "Enviar texto, imagem, áudio, vídeo, documento, reação"
      x-litezap-events: [message.sent, message.status]
      x-litezap-notes: "Instância desconectada não perde a mensagem: ela espera na fila até expires_after. Volume alto para contatos frios é o principal gatilho de banimento; veja Boas práticas."
      summary: Enviar mensagem
      x-litezap-scope: instancia
      description: |
        Valida e grava a mensagem na fila persistente da instância. Sem `wait`, responde `202` com
        `status: queued`. Com `?wait=true`, espera o resultado por até `timeout` (padrão `30s`, no máximo
        `2m`): `200` quando enviada, erro quando falhou, ou `202` com o status atual (`queued`/`in_flight`)
        se o tempo acabar; a mensagem continua na fila.

        Tipos: `text`, `image`, `video`, `audio` (`media.ptt: true` = mensagem de voz), `document`,
        `reaction`. `to` aceita número com DDI (10 a 15 dígitos) ou JID (`@s.whatsapp.net`, `@g.us`,
        `@lid`). Mídia por `media.url` (o servidor baixa, IPs privados bloqueados) ou `media.base64`.
        Limites: imagem 16MB, vídeo/áudio 64MB, documento 100MB, corpo 140MB, texto 64KB.

        Idempotência: `client_message_id` (até 128 caracteres) repetido devolve a mesma mensagem;
        repetido com outro conteúdo responde `409 conflict`.
      parameters:
        - name: wait
          in: query
          description: "`true` espera o envio terminar (modo síncrono)."
          schema: { type: string, enum: ["true"] }
        - name: timeout
          in: query
          description: "Duração Go (`30s`, `90s`, `2m`). Padrão `30s`, teto efetivo `2m`. Só com `wait=true`."
          schema: { type: string, example: 30s }
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/SendMessage" }
            examples:
              texto:
                summary: Texto com idempotência
                value: { to: "5511999999999", type: text, text: "Olá! Seu pedido saiu para entrega.", client_message_id: "pedido-123" }
              imagem:
                summary: Imagem por URL
                value: { to: "5511999999999", type: image, caption: "Comprovante", media: { url: "https://exemplo.com/comprovante.jpg" } }
              voz:
                summary: Áudio como mensagem de voz (base64)
                value: { to: "5511999999999", type: audio, media: { base64: "T2dnUwACAAAAAAAA...", mime: "audio/ogg; codecs=opus", ptt: true } }
              documento:
                summary: Documento
                value: { to: "5511999999999", type: document, caption: "Segue o boleto", media: { url: "https://exemplo.com/boleto.pdf", file_name: "boleto.pdf" } }
              reacao:
                summary: Reação
                value: { to: "5511999999999", type: reaction, reaction: { message_id: "msg_a41c9e07d2b35f68", emoji: "👍" } }
      responses:
        "200":
          description: Enviada (com `?wait=true`). Traz `wa_message_id`.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Message" }
              example:
                id: msg_a41c9e07d2b35f68
                client_message_id: pedido-123
                wa_message_id: 3EB0A1B2C3D4E5F6A7B8C9
                chat: 5511999999999@s.whatsapp.net
                from_me: true
                source: api
                type: text
                text: "Olá! Seu pedido saiu para entrega."
                status: sent
                timestamp: "2026-10-17T13:50:03Z"
        "202":
          description: Na fila (`queued`) ou em envio (`in_flight`).
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Message" }
              example:
                id: msg_a41c9e07d2b35f68
                client_message_id: pedido-123
                chat: 5511999999999@s.whatsapp.net
                from_me: true
                source: api
                type: text
                text: "Olá! Seu pedido saiu para entrega."
                status: queued
                timestamp: "2026-10-17T13:50:00Z"
        "400": { $ref: "#/components/responses/SendBadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/InstanceNotFound" }
        "409": { $ref: "#/components/responses/SendConflict" }
        "410": { $ref: "#/components/responses/MessageExpired" }
        "413": { $ref: "#/components/responses/MediaTooLarge" }
        "422": { $ref: "#/components/responses/MediaFetchFailed" }
        "502": { $ref: "#/components/responses/WorkerError" }
        "503": { $ref: "#/components/responses/BusyOrShuttingDown" }
        "504": { $ref: "#/components/responses/WorkerTimeout" }

  /v1/instances/{id}/messages/{msg_id}:
    parameters:
      - $ref: "#/components/parameters/InstanceID"
      - name: msg_id
        in: path
        required: true
        description: "`id` devolvido no envio (`msg_...`) ou id de uma mensagem do log."
        schema: { type: string }
    get:
      tags: [Mensagens]
      operationId: getMessage
      x-litezap-milestone: M2
      x-zapi: "(sem equivalente; status chega por webhook)"
      summary: Consultar mensagem
      x-litezap-scope: instancia
      description: |
        Status de um envio: `queued`, `in_flight`, `sent`, `delivered`, `read`, `played` ou `failed` (com `error`
        no formato `codigo: detalhe` e `attempts`). Também aceita o id de uma mensagem recebida do log.
      responses:
        "200":
          description: A mensagem.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Message" }
              examples:
                entregue:
                  value:
                    id: msg_a41c9e07d2b35f68
                    wa_message_id: 3EB0A1B2C3D4E5F6A7B8C9
                    chat: 5511999999999@s.whatsapp.net
                    from_me: true
                    source: api
                    type: text
                    text: "Olá!"
                    status: delivered
                    timestamp: "2026-10-17T13:50:03Z"
                falhou:
                  value:
                    id: msg_0c5e8b2f9a7d4613
                    chat: 5511888888888@s.whatsapp.net
                    from_me: true
                    source: api
                    type: text
                    text: "Olá!"
                    status: failed
                    attempts: 0
                    error: "invalid_number: number is not on WhatsApp"
                    timestamp: "2026-10-17T13:51:00Z"
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }

  /v1/instances/{id}/queue:
    parameters: [{ $ref: "#/components/parameters/InstanceID" }]
    get:
      tags: [Mensagens]
      operationId: listQueue
      x-litezap-sample: { query: { status: queued } }
      x-litezap-milestone: M6
      x-zapi: Fila
      summary: Listar a fila
      x-litezap-scope: instancia
      description: |
        Itens da fila de envio da instância, do mais antigo ao mais novo. Sem `status`, lista o que ainda
        não saiu (`queued` e `in_flight`); `?status=` filtra por `queued`, `in_flight`, `sent`, `failed`
        ou `all`. Pagina por `next_cursor`.
      parameters:
        - name: status
          in: query
          description: "`queued`, `in_flight`, `sent`, `failed` ou `all`. Omitido: `queued` e `in_flight`."
          schema: { type: string, enum: [queued, in_flight, sent, failed, all] }
        - name: limit
          in: query
          description: Itens por página. Padrão 50, máximo 200.
          schema: { type: integer, minimum: 1, maximum: 200, default: 50 }
        - $ref: "#/components/parameters/Cursor"
      responses:
        "200":
          description: Uma página da fila.
          content:
            application/json:
              schema:
                type: object
                properties:
                  items: { type: array, items: { $ref: "#/components/schemas/Message" } }
                  next_cursor: { type: string, description: Ausente na última página. }
              example:
                items:
                  - id: msg_a41c9e07d2b35f68
                    chat: 5511999999999@s.whatsapp.net
                    from_me: true
                    source: api
                    type: text
                    text: "Seu pedido saiu para entrega!"
                    status: queued
                    timestamp: "2026-10-17T13:50:00Z"
        "400": { $ref: "#/components/responses/ValidationFailed" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/InstanceNotFound" }
    delete:
      tags: [Mensagens]
      operationId: clearQueue
      x-litezap-milestone: M6
      x-zapi: Apagar fila
      summary: Esvaziar a fila
      x-litezap-scope: instancia
      x-litezap-events: [message.status]
      description: |
        Cancela de uma vez todos os itens `queued` da instância; os `in_flight` (sendo enviados agora)
        terminam normalmente. Cada item cancelado termina como `failed` com
        `error: "canceled: queue cleared by the API"` e sai `message.status` `failed`. Útil antes de
        reconectar uma instância que ficou parada com mensagens velhas na fila. Continua permitido com o
        envio suspenso pelo operador.
      responses:
        "200":
          description: Quantos itens foram cancelados.
          content:
            application/json:
              schema:
                type: object
                properties:
                  canceled: { type: integer }
              example: { canceled: 12 }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/InstanceNotFound" }

  /v1/instances/{id}/queue/{msg_id}:
    parameters:
      - $ref: "#/components/parameters/InstanceID"
      - name: msg_id
        in: path
        required: true
        description: "Id do item da fila (`msg_...`)."
        schema: { type: string }
    delete:
      tags: [Mensagens]
      operationId: cancelQueued
      x-litezap-milestone: M6
      x-zapi: Apagar mensagem da fila
      summary: Cancelar item da fila
      x-litezap-scope: instancia
      x-litezap-events: [message.status]
      description: |
        Remove um item ainda `queued` sem enviar: ele termina como `failed` com
        `error: "canceled: removed from the queue by the API"`, o arquivo de mídia preparado é apagado e
        sai `message.status` `failed`. Um item `in_flight` (sendo enviado agora) ou já terminado responde
        `409 conflict`.
      responses:
        "200":
          description: O item cancelado.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Message" }
              example:
                id: msg_a41c9e07d2b35f68
                chat: 5511999999999@s.whatsapp.net
                from_me: true
                source: api
                type: text
                text: "Seu pedido saiu para entrega!"
                status: failed
                error: "canceled: removed from the queue by the API"
                timestamp: "2026-10-17T13:50:00Z"
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "409":
          description: "`conflict`: o item já está sendo enviado (`in_flight`) ou terminou."
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
              example: { error: { code: conflict, message: "the message is in_flight and can no longer be removed from the queue", retryable: false } }

  /v1/instances/{id}/chats:
    parameters: [{ $ref: "#/components/parameters/InstanceID" }]
    get:
      tags: [Chats]
      operationId: listChats
      x-litezap-milestone: M2
      x-zapi: "Pegar chats"
      x-litezap-notes: "Chats sem nenhuma mensagem no log não aparecem."
      summary: Listar chats
      x-litezap-scope: instancia
      description: Chats do log de mensagens, do mais recente para o mais antigo. Paginação por cursor.
      parameters:
        - $ref: "#/components/parameters/Limit"
        - $ref: "#/components/parameters/Cursor"
      responses:
        "200":
          description: Uma página de chats.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ChatPage" }
              example:
                chats:
                  - jid: 5511999999999@s.whatsapp.net
                    name: Maria
                    is_group: false
                    last_message_at: "2026-10-17T13:52:10Z"
                    unread_count: 2
                  - jid: 120363000000000001@g.us
                    name: Equipe
                    is_group: true
                    last_message_at: "2026-10-17T12:00:00Z"
                    unread_count: 0
                next_cursor: eyJ0IjoiMjAyNi0xMC0xN1QxMjowMDowMFoifQ
        "400": { $ref: "#/components/responses/ValidationFailed" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/InstanceNotFound" }

  /v1/instances/{id}/chats/{jid}/messages:
    parameters:
      - $ref: "#/components/parameters/InstanceID"
      - $ref: "#/components/parameters/ChatJID"
    get:
      tags: [Chats]
      operationId: chatMessages
      x-litezap-milestone: M2
      x-zapi: "(sem equivalente)"
      summary: Histórico do chat
      x-litezap-scope: instancia
      description: |
        Mensagens do chat, da mais recente para a mais antiga. `status` é `received` para recebidas e
        `pending` → `sent` → `delivered` → `read` para enviadas (`pending` = em envio, sem confirmação
        do WhatsApp). Mídia recebida traz `media.url` para download.
      parameters:
        - $ref: "#/components/parameters/Limit"
        - $ref: "#/components/parameters/Cursor"
      responses:
        "200":
          description: Uma página de mensagens.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/MessagePage" }
              example:
                messages:
                  - id: msg_e29d4b8a0f6c1357
                    wa_message_id: 3A9F2C7D1E0B4A5C6D7E
                    chat: 5511999999999@s.whatsapp.net
                    sender: 5511999999999@s.whatsapp.net
                    from_me: false
                    source: contact
                    type: image
                    text: "Foto do produto"
                    media:
                      id: med_5b8e1f3c9d2a7046
                      kind: image
                      mime: image/jpeg
                      size: 182345
                      url: /v1/instances/inst_7f3a9c2e5b1d4086/media/med_5b8e1f3c9d2a7046
                    status: received
                    timestamp: "2026-10-17T13:52:10Z"
                next_cursor: eyJzIjoxMjM0fQ
        "400": { $ref: "#/components/responses/ValidationFailed" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/InstanceNotFound" }

  /v1/instances/{id}/chats/{jid}/read:
    parameters:
      - $ref: "#/components/parameters/InstanceID"
      - $ref: "#/components/parameters/ChatJID"
    post:
      tags: [Chats]
      operationId: markRead
      x-litezap-milestone: M2
      x-zapi: "Ler mensagens / Ler chats"
      summary: Marcar chat como lido
      x-litezap-scope: instancia
      description: Envia confirmação de leitura das últimas mensagens recebidas (até 100) e zera `unread_count`.
      responses:
        "204": { description: Marcado como lido. }
        "400": { $ref: "#/components/responses/ValidationFailed" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "409": { $ref: "#/components/responses/InstanceConflict" }
        "502": { $ref: "#/components/responses/WorkerError" }
        "504": { $ref: "#/components/responses/WorkerTimeout" }

  /v1/instances/{id}/contacts:
    parameters: [{ $ref: "#/components/parameters/InstanceID" }]
    get:
      tags: [Contatos]
      operationId: listContacts
      x-litezap-milestone: M2
      x-zapi: "Pegar contatos"
      summary: Listar contatos
      x-litezap-scope: instancia
      description: Contatos da sessão do WhatsApp (consulta o worker; a instância precisa estar rodando).
      responses:
        "200":
          description: Contatos ordenados por JID.
          content:
            application/json:
              schema:
                type: object
                properties:
                  contacts:
                    type: array
                    items: { $ref: "#/components/schemas/Contact" }
              example:
                contacts:
                  - jid: 5511999999999@s.whatsapp.net
                    name: Maria Souza
                    push_name: Maria
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/InstanceNotFound" }
        "409": { $ref: "#/components/responses/InstanceConflict" }
        "502": { $ref: "#/components/responses/WorkerError" }
        "504": { $ref: "#/components/responses/WorkerTimeout" }
  /v1/instances/{id}/contacts/{jid}:
    parameters:
      - $ref: "#/components/parameters/InstanceID"
      - $ref: "#/components/parameters/ChatJID"
    get:
      tags: [Contatos]
      operationId: getContact
      x-litezap-milestone: M2
      x-zapi: "Metadata do contato"
      summary: Consultar contato
      x-litezap-scope: instancia
      responses:
        "200":
          description: O contato.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Contact" }
              example:
                jid: 5511999999999@s.whatsapp.net
                name: Maria Souza
                push_name: Maria
                business_name: Loja da Maria
        "400": { $ref: "#/components/responses/ValidationFailed" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "409": { $ref: "#/components/responses/InstanceConflict" }
        "502": { $ref: "#/components/responses/WorkerError" }
        "504": { $ref: "#/components/responses/WorkerTimeout" }

  /v1/instances/{id}/chats/{jid}:
    parameters:
      - $ref: "#/components/parameters/InstanceID"
      - $ref: "#/components/parameters/ChatJID"
    get:
      tags: [Chats]
      operationId: getChat
      x-litezap-milestone: M9
      x-zapi: "Metadata do chat"
      summary: Consultar chat
      x-litezap-scope: instancia
      description: |
        Um chat do log com o estado do app state do WhatsApp (arquivado, fixado, silenciado, marcado como
        não lido). Com o worker rodando, o estado é antes reconciliado com a cópia local do worker, que
        inclui o sync inicial (sem eventos).
      responses:
        "200":
          description: O chat.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Chat" }
              example: { jid: 5511999999999@s.whatsapp.net, name: Maria, is_group: false, last_message_at: "2026-10-18T10:00:00Z", unread_count: 0, archived: false, pinned: true, muted: true, muted_until: "2026-10-18T18:00:00Z", unread: false }
        "400": { $ref: "#/components/responses/ValidationFailed" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "409": { $ref: "#/components/responses/InstanceConflict" }
        "502": { $ref: "#/components/responses/WorkerError" }
        "504": { $ref: "#/components/responses/WorkerTimeout" }
    patch:
      tags: [Chats]
      operationId: updateChat
      x-litezap-milestone: M9
      x-zapi: "Arquivar chat / Fixar chat / Silenciar chat / Ler chats"
      summary: Arquivar, fixar, silenciar ou marcar como (não) lido
      x-litezap-scope: instancia
      x-litezap-events: [chat.updated]
      description: |
        Muda o chat pelo app state do WhatsApp: o celular e os outros aparelhos acompanham (nada é enviado
        ao contato). Os campos são aplicados na ordem `unread`, `archived`, `pinned`, `muted`; uma falha
        para ali. `unread: false` também manda os recibos de leitura. Só chats que estão no log. O WhatsApp
        permite 3 chats fixados (`409 pin_limit`); arquivar desafixa. Logo depois de parear, até o celular
        mandar as chaves, responde `409 app_state_not_synced`.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/ChatPatch" }
            example: { muted: true, mute_seconds: 28800 }
      responses:
        "200":
          description: O chat depois da mudança.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Chat" }
        "400": { $ref: "#/components/responses/ValidationFailed" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "409": { $ref: "#/components/responses/ChatConflict" }
        "502": { $ref: "#/components/responses/WorkerError" }
        "504": { $ref: "#/components/responses/WorkerTimeout" }
    delete:
      tags: [Chats]
      operationId: deleteChat
      x-litezap-milestone: M9
      x-zapi: "Deletar chat"
      summary: Apagar chat
      x-litezap-scope: instancia
      x-litezap-events: [chat.updated]
      description: |
        Apaga a conversa no WhatsApp (celular e aparelhos vinculados). O log do LiteZap mantém as mensagens:
        o chat sai de `GET /chats` até chegar mensagem nova e `GET /chats/{jid}/messages` continua
        funcionando.
      parameters:
        - name: delete_media
          in: query
          schema: { type: string, enum: ["1"] }
          description: "`1` também apaga a mídia da conversa no celular."
      responses:
        "204": { description: Apagado. }
        "400": { $ref: "#/components/responses/ValidationFailed" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "409": { $ref: "#/components/responses/ChatConflict" }
        "502": { $ref: "#/components/responses/WorkerError" }
        "504": { $ref: "#/components/responses/WorkerTimeout" }
  /v1/instances/{id}/messages/{msg_id}/star:
    parameters:
      - $ref: "#/components/parameters/InstanceID"
      - name: msg_id
        in: path
        required: true
        schema: { type: string }
        description: Id da mensagem no LiteZap.
    put:
      tags: [Mensagens]
      operationId: starMessage
      x-litezap-milestone: M9
      summary: Favoritar mensagem
      x-litezap-scope: instancia
      x-litezap-events: [chat.updated]
      description: "Favorita (ou desfavorita) uma mensagem do log em todos os aparelhos da conta; a mensagem passa a ter `starred: true`."
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [starred]
              properties:
                starred: { type: boolean }
            example: { starred: true }
      responses:
        "200":
          description: A mensagem.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Message" }
        "400": { $ref: "#/components/responses/ValidationFailed" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "409": { $ref: "#/components/responses/ChatConflict" }
        "502": { $ref: "#/components/responses/WorkerError" }
        "504": { $ref: "#/components/responses/WorkerTimeout" }
  /v1/instances/{id}/contacts/{jid}/picture:
    parameters:
      - $ref: "#/components/parameters/InstanceID"
      - $ref: "#/components/parameters/ChatJID"
    get:
      tags: [Contatos]
      operationId: getContactPicture
      x-litezap-milestone: M9
      x-zapi: "Pegar imagem do contato"
      summary: Foto de perfil do contato
      x-litezap-scope: instancia
      description: |
        URL da foto de perfil de um contato ou grupo (CDN do WhatsApp; expira depois de um tempo) e seu id.
        Com `download=1` devolve o JPEG, baixado pelo worker (pelo proxy da instância) e guardado em cache
        por 1h. Sem foto, ou foto escondida pela privacidade do contato: `404 not_found` (nunca 502).
        Consulta o WhatsApp: gasta a cota de consultas de contato (30/min, 1000/dia); a URL fica em cache
        por 10 min.
      parameters:
        - name: preview
          in: query
          schema: { type: string, enum: ["1"] }
          description: Miniatura em vez da imagem inteira.
        - name: download
          in: query
          schema: { type: string, enum: ["1"] }
          description: Devolve o arquivo (image/jpeg) em vez do JSON.
      responses:
        "200":
          description: A foto.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Picture" }
              example: { jid: 5511999999999@s.whatsapp.net, id: "1729241234", url: "https://pps.whatsapp.net/v/t61.24694-24/...", type: image }
            image/jpeg:
              schema: { type: string, format: binary }
        "400": { $ref: "#/components/responses/ValidationFailed" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "409": { $ref: "#/components/responses/InstanceConflict" }
        "502": { $ref: "#/components/responses/WorkerError" }
        "504": { $ref: "#/components/responses/WorkerTimeout" }
        "429": { $ref: "#/components/responses/LookupRateLimited" }
  /v1/instances/{id}/contacts/{jid}/profile:
    parameters:
      - $ref: "#/components/parameters/InstanceID"
      - $ref: "#/components/parameters/ChatJID"
    get:
      tags: [Contatos]
      operationId: getContactProfile
      x-litezap-milestone: M9
      x-zapi: "Metadata do contato"
      summary: Recado e perfil comercial do contato
      x-litezap-scope: instancia
      description: Consulta o WhatsApp (gasta a cota de consultas de contato) e devolve o recado, o id da foto, o nome verificado e, em contas business, o perfil comercial.
      responses:
        "200":
          description: O perfil do contato.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ContactProfile" }
              example: { jid: 5511999999999@s.whatsapp.net, about: "Disponível", picture_id: "1729241234", verified_name: Loja da Maria, business: { email: contato@loja.com, categories: [Varejo] } }
        "400": { $ref: "#/components/responses/ValidationFailed" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "409": { $ref: "#/components/responses/InstanceConflict" }
        "502": { $ref: "#/components/responses/WorkerError" }
        "504": { $ref: "#/components/responses/WorkerTimeout" }
        "429": { $ref: "#/components/responses/LookupRateLimited" }
  /v1/instances/{id}/contacts/{jid}/block:
    parameters:
      - $ref: "#/components/parameters/InstanceID"
      - $ref: "#/components/parameters/ChatJID"
    post:
      tags: [Contatos]
      operationId: blockContact
      x-litezap-milestone: M9
      x-zapi: "Bloquear contato"
      summary: Bloquear contato
      x-litezap-scope: instancia
      description: Bloqueia o contato (vale também no celular). Devolve a lista de bloqueados.
      responses:
        "200":
          description: Bloqueados depois da mudança.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Blocklist" }
        "400": { $ref: "#/components/responses/ValidationFailed" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "409": { $ref: "#/components/responses/InstanceConflict" }
        "502": { $ref: "#/components/responses/WorkerError" }
        "504": { $ref: "#/components/responses/WorkerTimeout" }
  /v1/instances/{id}/contacts/{jid}/unblock:
    parameters:
      - $ref: "#/components/parameters/InstanceID"
      - $ref: "#/components/parameters/ChatJID"
    post:
      tags: [Contatos]
      operationId: unblockContact
      x-litezap-milestone: M9
      x-zapi: "Bloquear contato"
      summary: Desbloquear contato
      x-litezap-scope: instancia
      responses:
        "200":
          description: Bloqueados depois da mudança.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Blocklist" }
        "400": { $ref: "#/components/responses/ValidationFailed" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "409": { $ref: "#/components/responses/InstanceConflict" }
        "502": { $ref: "#/components/responses/WorkerError" }
        "504": { $ref: "#/components/responses/WorkerTimeout" }
  /v1/instances/{id}/blocklist:
    parameters: [{ $ref: "#/components/parameters/InstanceID" }]
    get:
      tags: [Contatos]
      operationId: getBlocklist
      x-litezap-milestone: M9
      summary: Listar bloqueados
      x-litezap-scope: instancia
      description: Contatos bloqueados (JID de telefone quando conhecido, senão o LID).
      responses:
        "200":
          description: Bloqueados.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Blocklist" }
              example: { blocked: [5511988887777@s.whatsapp.net] }
        "400": { $ref: "#/components/responses/ValidationFailed" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "409": { $ref: "#/components/responses/InstanceConflict" }
        "502": { $ref: "#/components/responses/WorkerError" }
        "504": { $ref: "#/components/responses/WorkerTimeout" }
  /v1/instances/{id}/profile:
    parameters: [{ $ref: "#/components/parameters/InstanceID" }]
    get:
      tags: [Perfil e privacidade]
      operationId: getProfile
      x-litezap-milestone: M9
      x-zapi: "Dados do celular / Dados da instância"
      summary: Perfil da conta
      x-litezap-scope: instancia
      responses:
        "200":
          description: O perfil.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Profile" }
              example: { jid: 5511999999999@s.whatsapp.net, name: Loja Exemplo, about: "Atendimento das 9h às 18h", platform: android, picture: { jid: 5511999999999@s.whatsapp.net, id: "1729241234", url: "https://pps.whatsapp.net/...", type: image } }
        "400": { $ref: "#/components/responses/ValidationFailed" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "409": { $ref: "#/components/responses/InstanceConflict" }
        "502": { $ref: "#/components/responses/WorkerError" }
        "504": { $ref: "#/components/responses/WorkerTimeout" }
    patch:
      tags: [Perfil e privacidade]
      operationId: updateProfile
      x-litezap-milestone: M9
      x-zapi: "Atualizar nome do perfil / Atualizar descrição do perfil"
      summary: Mudar nome e recado
      x-litezap-scope: instancia
      description: "`name` (1 a 25 caracteres) é o nome que aparece para quem não salvou o número; `about` (até 139) é o recado."
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/ProfilePatch" }
            example: { name: Loja Exemplo, about: "Atendimento das 9h às 18h" }
      responses:
        "200":
          description: O perfil depois da mudança.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Profile" }
        "400": { $ref: "#/components/responses/ValidationFailed" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "409": { $ref: "#/components/responses/InstanceConflict" }
        "502": { $ref: "#/components/responses/WorkerError" }
        "504": { $ref: "#/components/responses/WorkerTimeout" }
  /v1/instances/{id}/profile/picture:
    parameters: [{ $ref: "#/components/parameters/InstanceID" }]
    get:
      tags: [Perfil e privacidade]
      operationId: getProfilePicture
      x-litezap-milestone: M9
      summary: Baixar a foto da conta
      x-litezap-scope: instancia
      description: "JPEG da foto da própria conta (pelo proxy da instância; não gasta a cota de consultas). `404 not_found` sem foto."
      parameters:
        - name: preview
          in: query
          schema: { type: string, enum: ["1"] }
      responses:
        "200":
          description: A foto.
          content:
            image/jpeg:
              schema: { type: string, format: binary }
        "400": { $ref: "#/components/responses/ValidationFailed" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "409": { $ref: "#/components/responses/InstanceConflict" }
        "502": { $ref: "#/components/responses/WorkerError" }
        "504": { $ref: "#/components/responses/WorkerTimeout" }
    put:
      tags: [Perfil e privacidade]
      operationId: setProfilePicture
      x-litezap-milestone: M9
      x-zapi: "Atualizar foto do perfil"
      summary: Trocar a foto da conta
      x-litezap-scope: instancia
      description: JPEG, PNG ou GIF (até 10 MB) por URL pública ou base64; recortado no centro e reduzido a 640x640 JPEG (WebP não).
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/PictureInput" }
            example: { url: "https://exemplo.com/logo.png" }
      responses:
        "200":
          description: O perfil com a foto nova.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Profile" }
        "400": { $ref: "#/components/responses/ValidationFailed" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "409": { $ref: "#/components/responses/InstanceConflict" }
        "502": { $ref: "#/components/responses/WorkerError" }
        "504": { $ref: "#/components/responses/WorkerTimeout" }
        "413": { description: "`media_too_large`: a imagem passa de 10 MB." }
    delete:
      tags: [Perfil e privacidade]
      operationId: removeProfilePicture
      x-litezap-milestone: M9
      summary: Remover a foto da conta
      x-litezap-scope: instancia
      responses:
        "204": { description: Removida. }
        "400": { $ref: "#/components/responses/ValidationFailed" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "409": { $ref: "#/components/responses/InstanceConflict" }
        "502": { $ref: "#/components/responses/WorkerError" }
        "504": { $ref: "#/components/responses/WorkerTimeout" }
  /v1/instances/{id}/privacy:
    parameters: [{ $ref: "#/components/parameters/InstanceID" }]
    get:
      tags: [Perfil e privacidade]
      operationId: getPrivacy
      x-litezap-milestone: M9
      x-zapi: "Privacidade"
      summary: Configurações de privacidade
      x-litezap-scope: instancia
      responses:
        "200":
          description: As configurações, como o WhatsApp as tem.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Privacy" }
              example: { last_seen: contacts, online: all, profile_photo: all, about: contacts, groups_add: contacts, read_receipts: all, calls: all, messages: all }
        "400": { $ref: "#/components/responses/ValidationFailed" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "409": { $ref: "#/components/responses/InstanceConflict" }
        "502": { $ref: "#/components/responses/WorkerError" }
        "504": { $ref: "#/components/responses/WorkerTimeout" }
    patch:
      tags: [Perfil e privacidade]
      operationId: updatePrivacy
      x-litezap-milestone: M9
      x-zapi: "Visto por último / Foto / Recado / Grupos / Online / Confirmação de leitura / Mensagens temporárias"
      summary: Mudar a privacidade
      x-litezap-scope: instancia
      description: |
        Cada campo é aplicado em seguida (uma falha para ali). Valores: `all`, `contacts`,
        `contact_blacklist`, `none` (`last_seen`, `profile_photo`, `about`, `groups_add`); `all`/`none`
        (`read_receipts`); `all`/`match_last_seen` (`online`); `all`/`known` (`calls`); `all`/`contacts`
        (`messages`). `default_disappearing_seconds` (0, 86400, 604800 ou 7776000) vale para conversas
        novas. As listas de exceção ("meus contatos, exceto…") só no celular.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/PrivacyPatch" }
            example: { last_seen: contacts, read_receipts: none }
      responses:
        "200":
          description: As configurações depois da mudança.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Privacy" }
        "400": { $ref: "#/components/responses/ValidationFailed" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "409": { $ref: "#/components/responses/InstanceConflict" }
        "502": { $ref: "#/components/responses/WorkerError" }
        "504": { $ref: "#/components/responses/WorkerTimeout" }

  /v1/instances/{id}/check-numbers:
    parameters: [{ $ref: "#/components/parameters/InstanceID" }]
    post:
      tags: [Números]
      operationId: checkNumbers
      x-litezap-milestone: M2
      x-zapi: "Número com WhatsApp / Validação em lote"
      x-litezap-notes: "Consultas em massa de números são um padrão de risco; por isso as cotas são baixas de propósito."
      summary: Verificar números
      x-litezap-scope: instancia
      description: |
        Diz quais números têm WhatsApp e devolve o `jid` que o WhatsApp usa (no Brasil, com ou sem o 9º
        dígito). De 1 a 10 números por chamada (10 a 15 dígitos com DDI). Resultados ficam em cache
        (30 dias para quem tem, 24h para quem não tem). Consultas não cacheadas: até 10 por minuto e
        200 por dia (UTC) por instância; acima disso `429 rate_limited`.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [phones]
              properties:
                phones:
                  type: array
                  minItems: 1
                  maxItems: 10
                  items: { type: string }
            example: { phones: ["5511999999999", "5511888888888"] }
      responses:
        "200":
          description: Resultado por número.
          content:
            application/json:
              schema:
                type: object
                properties:
                  results:
                    type: array
                    items: { $ref: "#/components/schemas/NumberResult" }
              example:
                results:
                  - { phone: "5511999999999", exists: true, jid: "5511999999999@s.whatsapp.net" }
                  - { phone: "5511888888888", exists: false }
        "400": { $ref: "#/components/responses/ValidationFailed" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/InstanceNotFound" }
        "409": { $ref: "#/components/responses/InstanceConflict" }
        "429": { $ref: "#/components/responses/RateLimited" }
        "502": { $ref: "#/components/responses/WorkerError" }
        "504": { $ref: "#/components/responses/WorkerTimeout" }

  /v1/instances/{id}/media/{media_id}:
    parameters:
      - $ref: "#/components/parameters/InstanceID"
      - $ref: "#/components/parameters/MediaID"
    get:
      tags: [Mídia]
      operationId: getMedia
      x-litezap-sample: { binary: arquivo }
      x-litezap-milestone: M2
      x-zapi: "(mídia chega por URL no webhook)"
      x-litezap-notes: "Mídia antiga pode não estar mais disponível no WhatsApp (410)."
      summary: Baixar mídia recebida
      x-litezap-scope: instancia
      description: |
        Baixa a mídia de uma mensagem recebida (`media.id` no log). Na primeira vez o worker baixa do
        WhatsApp e o arquivo fica em `data/media/`. Com `?signed=1` devolve uma URL assinada que vale 1h
        sem token, para repassar a terceiros. Fora de loopback, `?signed=1` exige `public_url`
        (`409 conflict`). A resposta leva `X-Content-Type-Options: nosniff`, CSP `sandbox` e
        `Content-Disposition` (`inline` só para JPEG/PNG/WebP/GIF, áudio e vídeo).
      parameters:
        - name: signed
          in: query
          description: "`1` devolve `{url, expires_at}` em vez do arquivo."
          schema: { type: string, enum: ["1"] }
      responses:
        "200":
          description: O arquivo, ou a URL assinada com `?signed=1`.
          content:
            application/octet-stream:
              schema: { type: string, format: binary }
            application/json:
              schema: { $ref: "#/components/schemas/SignedURL" }
              example:
                url: "https://api.seu-dominio.com/media/inst_7f3a9c2e5b1d4086/med_5b8e1f3c9d2a7046?exp=1792252800&sig=9f86d081884c7d65"
                expires_at: "2026-10-17T14:52:10Z"
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "409": { $ref: "#/components/responses/SignedDisabled" }
        "410": { $ref: "#/components/responses/MediaUnavailable" }
        "502": { $ref: "#/components/responses/WorkerError" }
        "504": { $ref: "#/components/responses/WorkerTimeout" }

  /media/{id}/{media_id}:
    parameters:
      - $ref: "#/components/parameters/InstanceID"
      - $ref: "#/components/parameters/MediaID"
    get:
      tags: [Mídia]
      operationId: getSignedMedia
      x-litezap-sample: { query: { exp: "1792252800", sig: 9f86d081884c7d65 }, binary: arquivo }
      x-litezap-milestone: M2
      x-zapi: "(sem equivalente)"
      summary: Mídia por URL assinada
      x-litezap-scope: assinatura
      security: []
      description: Rota fora de `/v1`, sem token. A assinatura HMAC-SHA256 (segredo em `data/media_key`) é a credencial. Apagar `data/media_key` invalida todas as URLs já emitidas.
      parameters:
        - { name: exp, in: query, required: true, description: Expiração (unix)., schema: { type: string } }
        - { name: sig, in: query, required: true, description: Assinatura., schema: { type: string } }
      responses:
        "200":
          description: O arquivo.
          content:
            application/octet-stream:
              schema: { type: string, format: binary }
        "403":
          description: "`forbidden`: URL inválida ou expirada."
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
              example: { error: { code: forbidden, message: "invalid or expired media URL", retryable: false } }
        "404": { $ref: "#/components/responses/NotFound" }
        "410": { $ref: "#/components/responses/MediaUnavailable" }

  /v1/instances/{id}/agent-hook:
    parameters: [{ $ref: "#/components/parameters/InstanceID" }]
    get:
      tags: [Atendente IA]
      operationId: getAgentHook
      x-litezap-milestone: M5
      summary: Ler hook de agente
      x-litezap-scope: instancia
      description: "Configuração do hook (o segredo nunca volta; `secret_set` diz se existe). Sem configuração, devolve os padrões com `configured: false`."
      responses:
        "200":
          description: Configuração.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/AgentHook" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/InstanceNotFound" }
    put:
      tags: [Atendente IA]
      operationId: putAgentHook
      x-litezap-milestone: M5
      summary: Configurar hook de agente
      x-litezap-scope: instancia
      x-litezap-events: [agent.failed, agent.handoff, agent.lead, agent.appointment_requested, agent.order_draft]
      description: |
        Atualização parcial (cria se não existir). `target: "builtin"` usa o Atendente LiteZap embutido (sem
        URL); `target: "url"` (padrão) faz um `POST` assinado para `url`. Um `secret` ausente é gerado e
        aparece só nesta resposta. Campos desconhecidos são recusados.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/AgentHookPatch" }
            examples:
              embutido: { summary: Atendente LiteZap, value: { target: builtin, enabled: true } }
              externo: { summary: Agente externo, value: { target: url, url: "https://meu-agente.exemplo.com/litezap", enabled: true } }
      responses:
        "200":
          description: Configuração salva (`secret` só quando acabou de ser gerado).
          content:
            application/json:
              schema: { $ref: "#/components/schemas/AgentHook" }
        "400": { $ref: "#/components/responses/ValidationFailed" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/InstanceNotFound" }
    delete:
      tags: [Atendente IA]
      operationId: deleteAgentHook
      x-litezap-milestone: M5
      summary: Remover hook de agente
      x-litezap-scope: instancia
      description: Remove a configuração (o atendente para); as regras por conversa continuam.
      responses:
        "204": { description: Removido. }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
  /v1/instances/{id}/chats/{jid}/agent:
    parameters: [{ $ref: "#/components/parameters/InstanceID" }, { $ref: "#/components/parameters/ChatJID" }]
    put:
      tags: [Atendente IA]
      operationId: putChatAgent
      x-litezap-milestone: M5
      summary: Ligar ou desligar o atendente numa conversa
      x-litezap-scope: instancia
      description: '`{"enabled": true | false | null}`; `null` herda o padrão da instância. `true` e `null` também encerram uma pausa (após handoff ou resposta humana).'
      requestBody:
        required: true
        content:
          application/json:
            schema: { type: object, required: [enabled], properties: { enabled: { type: [boolean, "null"] } } }
            example: { enabled: false }
      responses:
        "200":
          description: Estado do atendente na conversa.
          content:
            application/json:
              schema: { type: object, properties: { jid: { type: string }, name: { type: string }, is_group: { type: boolean }, enabled: { type: [boolean, "null"] }, paused_until: { type: string, format: date-time } } }
        "400": { $ref: "#/components/responses/ValidationFailed" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/InstanceNotFound" }
  /v1/instances/{id}/agent-hook/calls:
    parameters: [{ $ref: "#/components/parameters/InstanceID" }]
    get:
      tags: [Atendente IA]
      operationId: listAgentCalls
      x-litezap-milestone: M5
      summary: Últimas chamadas do atendente
      x-litezap-scope: instancia
      description: "Log das chamadas (`ok`, `failed`, `skipped`, `discarded`, `paused`), com motivo e duração. Motivos do embutido: `not_configured`, `daily_cap`, `llm_error`, `invalid_response`."
      parameters: [{ $ref: "#/components/parameters/Limit" }]
      responses:
        "200":
          description: Chamadas, mais recente primeiro.
          content:
            application/json:
              schema: { type: object, properties: { calls: { type: array, items: { type: object } } } }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/InstanceNotFound" }
  /v1/instances/{id}/attendant:
    parameters: [{ $ref: "#/components/parameters/InstanceID" }]
    get:
      tags: [Atendente IA]
      operationId: getAttendant
      x-litezap-milestone: M5b
      summary: Ler o Atendente LiteZap
      x-litezap-scope: instancia
      description: Provedor, modelo, limites e uso de hoje. As chaves nunca voltam (`api_key_set`, `audio_api_key_set`).
      responses:
        "200":
          description: Configuração do atendente embutido.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/AttendantSettings" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/InstanceNotFound" }
    put:
      tags: [Atendente IA]
      operationId: putAttendant
      x-litezap-milestone: M5b
      summary: Configurar provedor do atendente
      x-litezap-scope: admin
      x-litezap-notes: "Cada mensagem atendida é uma chamada paga ao provedor: mantenha daily_call_cap."
      description: |
        Só o token admin. Atualização parcial: `provider` (`anthropic` | `openai`, qualquer servidor
        OpenAI-compatível), `model` (padrão `claude-sonnet-5-5`), `base_url`, `api_key` (cifrada em
        repouso com `data/attendant_key`; `""` remove), `daily_call_cap` (padrão 300 chamadas/dia, inclui
        transcrição e voz), `max_tokens`, `vision`, `audio` (servidor OpenAI-compatível de
        `/audio/transcriptions` e `/audio/speech`) e preços para o log de custo.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/AttendantSettingsPatch" }
            examples:
              anthropic: { summary: Claude (padrão), value: { provider: anthropic, api_key: "sk-ant-...", daily_call_cap: 300 } }
              local: { summary: Servidor local (Ollama), value: { provider: openai, base_url: "http://127.0.0.1:11434/v1", model: "llama3.1" } }
      responses:
        "200":
          description: Configuração salva (sem as chaves).
          content:
            application/json:
              schema: { $ref: "#/components/schemas/AttendantSettings" }
        "400": { $ref: "#/components/responses/ValidationFailed" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/InstanceNotFound" }
  /v1/instances/{id}/attendant/presets:
    parameters: [{ $ref: "#/components/parameters/InstanceID" }]
    get:
      tags: [Atendente IA]
      operationId: attendantPresets
      x-litezap-milestone: M5b
      summary: Modos prontos do atendente
      x-litezap-scope: instancia
      description: "Objetivo, roteiro, tom, critérios de passagem para humano e ações de cada modo: `vendedor`, `pedido`, `sdr`, `suporte`, `custom`."
      responses:
        "200":
          description: Presets por modo.
          content:
            application/json:
              schema: { type: object, properties: { presets: { type: object, additionalProperties: { $ref: "#/components/schemas/AttendantProfile" } } } }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
  /v1/instances/{id}/attendant/agents:
    parameters: [{ $ref: "#/components/parameters/InstanceID" }]
    get:
      tags: [Atendente IA]
      operationId: listAttendantAgents
      x-litezap-milestone: M5b
      summary: Listar agentes
      x-litezap-scope: instancia
      description: Agentes da instância; `active` marca o que responde (o escolhido, senão o primeiro criado).
      responses:
        "200":
          description: Agentes.
          content:
            application/json:
              schema: { type: object, properties: { agents: { type: array, items: { $ref: "#/components/schemas/AttendantAgent" } } } }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/InstanceNotFound" }
    post:
      tags: [Atendente IA]
      operationId: createAttendantAgent
      x-litezap-milestone: M5b
      summary: Criar agente
      x-litezap-scope: instancia
      description: Campos de roteiro ausentes vêm do preset do modo; tudo é editável depois. Até 20 agentes por instância.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/AttendantProfile" }
            example:
              name: Bia da Pizzaria
              mode: pedido
              business: "Pizzaria do Zé, Rua A 10, Centro. Entrega em até 40 min. Pix ou cartão na entrega."
              knowledge:
                - { kind: product, title: Pizza Margherita, price: 45.0, content: "8 fatias" }
                - { kind: hours, title: Seg a Dom, content: "18h às 23h" }
                - { kind: faq, title: "Tem opção sem glúten?", content: "Sim, massa sem glúten por +R$ 8." }
      responses:
        "201":
          description: Agente criado.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/AttendantAgent" }
        "400": { $ref: "#/components/responses/ValidationFailed" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/InstanceNotFound" }
  /v1/instances/{id}/attendant/agents/{agent_id}:
    parameters:
      - { $ref: "#/components/parameters/InstanceID" }
      - { name: agent_id, in: path, required: true, description: "Id do agente (`agt_...`).", schema: { type: string } }
    get:
      tags: [Atendente IA]
      operationId: getAttendantAgent
      x-litezap-milestone: M5b
      summary: Ler agente
      x-litezap-scope: instancia
      responses:
        "200":
          description: Agente.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/AttendantAgent" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
    put:
      tags: [Atendente IA]
      operationId: updateAttendantAgent
      x-litezap-milestone: M5b
      summary: Alterar agente
      x-litezap-scope: instancia
      description: Substitui o perfil inteiro (um roteiro vazio fica vazio).
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/AttendantProfile" }
      responses:
        "200":
          description: Agente alterado.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/AttendantAgent" }
        "400": { $ref: "#/components/responses/ValidationFailed" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
    delete:
      tags: [Atendente IA]
      operationId: deleteAttendantAgent
      x-litezap-milestone: M5b
      summary: Excluir agente
      x-litezap-scope: instancia
      responses:
        "204": { description: Excluído. }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
  /v1/instances/{id}/attendant/agents/{agent_id}/activate:
    parameters:
      - { $ref: "#/components/parameters/InstanceID" }
      - { name: agent_id, in: path, required: true, description: "Id do agente (`agt_...`).", schema: { type: string } }
    post:
      tags: [Atendente IA]
      operationId: activateAttendantAgent
      x-litezap-milestone: M5b
      summary: Ativar agente
      x-litezap-scope: instancia
      description: Faz este agente responder as conversas da instância.
      responses:
        "200":
          description: Agente ativo.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/AttendantAgent" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
  /v1/instances/{id}/attendant/simulate:
    parameters: [{ $ref: "#/components/parameters/InstanceID" }]
    post:
      tags: [Atendente IA]
      operationId: simulateAttendant
      x-litezap-milestone: M5b
      summary: Simular conversa
      x-litezap-scope: instancia
      description: |
        Roda um agente (salvo, o ativo, ou um perfil ainda não salvo em `agent`) contra mensagens de exemplo
        **sem enviar nada**. `from`: `customer` (padrão), `agent`, `human`, `automatic`. A última mensagem
        precisa ser do cliente (ou use `trigger: "call"` para a mensagem pós-ligação). É uma chamada real ao
        provedor e conta no limite diário.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                agent_id: { type: string }
                agent: { $ref: "#/components/schemas/AttendantProfile" }
                messages: { type: array, maxItems: 50, items: { type: object, properties: { from: { type: string, enum: [customer, agent, human, automatic] }, text: { type: string } } } }
                trigger: { type: string, enum: [call] }
                contact_name: { type: string }
            example: { messages: [{ from: automatic, text: "Não atendemos ligações, mande mensagem." }, { text: "quero 2 margheritas pra Rua B 5" }] }
      responses:
        "200":
          description: O que o agente faria.
          content:
            application/json:
              schema:
                type: object
                properties:
                  reply: { type: string }
                  voice: { type: boolean }
                  handoff: { type: boolean }
                  handoff_note: { type: string }
                  events: { type: array, items: { type: object } }
                  model: { type: string }
                  usage: { type: object, properties: { input_tokens: { type: integer }, output_tokens: { type: integer }, cost_usd: { type: number } } }
              example: { reply: "Resumo: 2x Pizza Margherita (R$ 90,00), entrega na Rua B, 5. Confirma?", voice: false, handoff: false, events: [{ type: agent.order_draft, items: [{ name: Pizza Margherita, quantity: 2, unit_price: 45, subtotal: 90, in_catalog: true }], total: 90, currency: BRL, address: "Rua B, 5" }], model: claude-sonnet-5-5, usage: { input_tokens: 1830, output_tokens: 120, cost_usd: 0.00486 } }
        "400": { $ref: "#/components/responses/ValidationFailed" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
  /v1/instances/{id}/attendant/usage:
    parameters: [{ $ref: "#/components/parameters/InstanceID" }]
    get:
      tags: [Atendente IA]
      operationId: attendantUsage
      x-litezap-milestone: M5b
      summary: Uso e custo do atendente
      x-litezap-scope: instancia
      description: Chamadas ao provedor, tokens e custo estimado (US$) por dia UTC, mais recente primeiro.
      parameters: [{ name: days, in: query, description: "Dias (padrão 7, máximo 90).", schema: { type: integer, minimum: 1, maximum: 90 } }]
      responses:
        "200":
          description: Uso por dia.
          content:
            application/json:
              schema: { type: object, properties: { usage: { type: array, items: { type: object, properties: { day: { type: string }, calls: { type: integer }, input_tokens: { type: integer }, output_tokens: { type: integer }, cost_usd: { type: number } } } } } }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/InstanceNotFound" }

  /v1/webhooks:
    post:
      tags: [Webhooks]
      operationId: createWebhook
      x-litezap-milestone: M3
      x-zapi: "Atualizar webhook (ao receber, ao enviar, status...)"
      summary: Criar webhook
      x-litezap-scope: instancia
      description: |
        Cadastra um endpoint que recebe por `POST` os eventos que combinam com `events` (tipos exatos,
        famílias como `message.*`, ou `*`). Com token de instância o webhook é sempre daquela instância.
        Com token admin, `instance_id` escolhe a instância; sem ele o webhook é **global** (todas as
        instâncias).

        `secret` é opcional (16 a 256 caracteres); sem ele um é gerado com prefixo `whsec_`. O segredo só
        aparece nesta resposta (e na de um `PATCH` que o troque): guarde-o. Só URLs `http(s)`; endereços
        privados, loopback e link-local são recusados na entrega, salvo `webhook_allow_private_urls: true`.
        Redirecionamentos não são seguidos.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/WebhookInput" }
            example: { url: "https://exemplo.com/litezap", events: ["message.*", "instance.status"] }
      responses:
        "201":
          description: Webhook criado, com o `secret`.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Webhook" }
              example:
                id: wh_3c7e1a9f5b2d4806
                instance_id: inst_7f3a9c2e5b1d4086
                url: https://exemplo.com/litezap
                events: ["message.*", "instance.status"]
                version: 1
                enabled: true
                created_at: "2026-10-17T14:00:00Z"
                updated_at: "2026-10-17T14:00:00Z"
                secret: whsec_9Jd2kQm4xV7pR1tY6nB3cF8hL0sW5zA
        "400": { $ref: "#/components/responses/ValidationFailed" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/InstanceNotFound" }
    get:
      tags: [Webhooks]
      operationId: listWebhooks
      x-litezap-milestone: M3
      x-zapi: "(sem equivalente)"
      summary: Listar webhooks
      x-litezap-scope: instancia
      description: |
        Com token de instância, lista os webhooks da própria instância. Com token admin, lista todos
        (globais e de instâncias); `instance_id` filtra por uma instância. O `secret` nunca aparece aqui.
      parameters:
        - name: instance_id
          in: query
          description: "Só admin: filtra pelos webhooks desta instância."
          schema: { type: string }
      responses:
        "200":
          description: Webhooks visíveis para o token.
          content:
            application/json:
              schema:
                type: object
                properties:
                  webhooks: { type: array, items: { $ref: "#/components/schemas/Webhook" } }
              example:
                webhooks:
                  - id: wh_3c7e1a9f5b2d4806
                    instance_id: inst_7f3a9c2e5b1d4086
                    url: https://exemplo.com/litezap
                    events: ["message.*", "instance.status"]
                    version: 1
                    enabled: true
                    created_at: "2026-10-17T14:00:00Z"
                    updated_at: "2026-10-17T14:00:00Z"
        "401": { $ref: "#/components/responses/Unauthorized" }

  /v1/webhooks/{wid}:
    parameters: [{ $ref: "#/components/parameters/WebhookID" }]
    get:
      tags: [Webhooks]
      operationId: getWebhook
      x-litezap-milestone: M3
      x-zapi: "(sem equivalente)"
      summary: Consultar webhook
      x-litezap-scope: instancia
      description: Um webhook de outra instância responde `404 not_found` para o token de instância.
      responses:
        "200":
          description: O webhook (sem o `secret`).
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Webhook" }
              example:
                id: wh_3c7e1a9f5b2d4806
                instance_id: inst_7f3a9c2e5b1d4086
                url: https://exemplo.com/litezap
                events: ["message.*", "instance.status"]
                version: 1
                enabled: true
                created_at: "2026-10-17T14:00:00Z"
                updated_at: "2026-10-17T14:00:00Z"
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/WebhookNotFound" }
    patch:
      tags: [Webhooks]
      operationId: updateWebhook
      x-litezap-milestone: M3
      x-zapi: "Atualizar todos os webhooks"
      summary: Atualizar webhook
      x-litezap-scope: instancia
      description: |
        Atualização parcial de `url`, `events`, `enabled` e `secret`. `instance_id` não muda (crie outro
        webhook). `{"secret": ""}` gera um segredo novo, devolvido nesta resposta. Desligar
        (`"enabled": false`) para de gerar entregas novas e pausa as pendentes até religar.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/WebhookInput" }
            example: { events: ["*"], enabled: true }
      responses:
        "200":
          description: O webhook atualizado (com `secret` só se ele foi trocado).
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Webhook" }
        "400": { $ref: "#/components/responses/ValidationFailed" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/WebhookNotFound" }
    delete:
      tags: [Webhooks]
      operationId: deleteWebhook
      x-litezap-milestone: M3
      x-zapi: "(sem equivalente)"
      summary: Apagar webhook
      x-litezap-scope: instancia
      description: Apaga o webhook e as entregas dele.
      responses:
        "204": { description: Apagado. }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/WebhookNotFound" }

  /v1/webhooks/{wid}/deliveries:
    parameters: [{ $ref: "#/components/parameters/WebhookID" }]
    get:
      tags: [Webhooks]
      operationId: listDeliveries
      x-litezap-sample: { query: { status: failed } }
      x-litezap-milestone: M3
      x-zapi: "(sem equivalente)"
      summary: Listar entregas
      x-litezap-scope: instancia
      description: |
        Entregas do webhook, mais recentes primeiro, com tentativas, último status HTTP, último erro e o
        evento completo. `?status=failed` é a dead-letter: entregas que ficaram 24 horas sem sucesso.
        Entregas concluídas ficam 7 dias e a dead-letter 30 dias (configurável).
      parameters:
        - name: status
          in: query
          description: "`pending`, `delivered` ou `failed`. Omitido: todas."
          schema: { type: string, enum: [pending, delivered, failed] }
        - name: limit
          in: query
          description: Itens por página. Padrão 50, máximo 100.
          schema: { type: integer, minimum: 1, maximum: 100, default: 50 }
        - $ref: "#/components/parameters/Cursor"
      responses:
        "200":
          description: Uma página de entregas.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/DeliveryPage" }
              example:
                deliveries:
                  - id: dlv_8b1f4e2a6c9d3075
                    webhook_id: wh_3c7e1a9f5b2d4806
                    event_id: evt_5f2c9a1b7d3e4f60
                    event_type: message.received
                    instance_id: inst_7f3a9c2e5b1d4086
                    status: failed
                    attempts: 19
                    expires_at: "2026-10-18T14:02:11Z"
                    last_status_code: 503
                    last_error: "http 503"
                    created_at: "2026-10-17T14:02:11Z"
                    updated_at: "2026-10-18T14:02:11Z"
                    event: { id: evt_5f2c9a1b7d3e4f60, type: message.received, version: 1, instance_id: inst_7f3a9c2e5b1d4086, timestamp: "2026-10-17T14:02:11.204Z", data: { id: msg_e29d4b8a0f6c1357, chat: 5511999999999@s.whatsapp.net, from_me: false, source: contact, type: text, text: "Oi!", timestamp: "2026-10-17T14:02:10Z" } }
                next_cursor: eyJzIjo0Mn0
        "400": { $ref: "#/components/responses/ValidationFailed" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/WebhookNotFound" }

  /v1/webhooks/{wid}/deliveries/{delivery_id}/retry:
    parameters:
      - $ref: "#/components/parameters/WebhookID"
      - name: delivery_id
        in: path
        required: true
        description: "Id da entrega (`dlv_...`)."
        schema: { type: string }
    post:
      tags: [Webhooks]
      operationId: retryDelivery
      x-litezap-milestone: M3
      x-zapi: "(sem equivalente)"
      summary: Reenviar entrega
      x-litezap-scope: instancia
      description: Recoloca uma entrega da dead-letter (`failed`) na fila, com mais 24 horas de prazo. Só entregas `failed` podem ser reenviadas.
      responses:
        "200":
          description: A entrega, de volta a `pending`.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Delivery" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/WebhookNotFound" }
        "409": { $ref: "#/components/responses/RetryConflict" }
        "503": { $ref: "#/components/responses/ShuttingDown" }

  /v1/instances/{id}/presence/subscribe:
    parameters: [{ $ref: "#/components/parameters/InstanceID" }]
    post:
      tags: [Eventos em tempo real]
      operationId: subscribePresence
      x-litezap-milestone: M7
      x-zapi: "Status do chat (presença)"
      x-litezap-notes: "Exige presence.online, que silencia as notificações do celular."
      summary: Assinar presença de um contato
      x-litezap-scope: instancia
      description: |
        Pede ao WhatsApp as mudanças de online/offline (e visto por último, se o contato mostra) de um
        contato; chegam como `chat.presence` com `state` `online`/`offline`. Exige
        `presence.online` nas settings (`409 presence_offline`). Até 20 assinaturas por minuto e 200 por dia por
        instância (`429 rate_limited`); recusada com `403 sending_suspended` enquanto o operador suspende
        envios. Valem até a conexão cair.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [to]
              properties:
                to: { type: string, description: "Número com DDI ou JID de contato (grupos não têm presença)." }
            example: { to: "5511999999999" }
      responses:
        "204": { description: Assinado. }
        "400": { $ref: "#/components/responses/ValidationFailed" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/InstanceNotFound" }
        "409": { $ref: "#/components/responses/InstanceConflict" }
        "429": { $ref: "#/components/responses/RateLimited" }
        "502": { $ref: "#/components/responses/WorkerError" }

  /v1/instances/{id}/groups:
    parameters: [{ $ref: "#/components/parameters/InstanceID" }]
    get:
      tags: [Grupos]
      operationId: listGroups
      x-litezap-milestone: M8
      x-zapi: "Buscar grupos"
      summary: Listar grupos
      x-litezap-scope: instancia
      description: Grupos em que o número está, com configurações e número de participantes (sem a lista de participantes; use `GET /groups/{jid}`).
      responses:
        "200":
          description: Grupos.
          content:
            application/json:
              schema:
                type: object
                properties:
                  groups: { type: array, items: { $ref: "#/components/schemas/Group" } }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/InstanceNotFound" }
        "409": { $ref: "#/components/responses/InstanceConflict" }
        "502": { $ref: "#/components/responses/WorkerError" }
        "504": { $ref: "#/components/responses/WorkerTimeout" }
    post:
      tags: [Grupos]
      operationId: createGroup
      x-litezap-milestone: M8
      x-zapi: "Criar grupo"
      x-litezap-events: [group.updated]
      x-litezap-notes: "Criar grupos com desconhecidos é gatilho de banimento: cota diária e participantes só conhecidos."
      summary: Criar grupo
      x-litezap-scope: instancia
      description: |
        Cria um grupo com até 10 participantes iniciais (número com DDI ou JID). Só entram pessoas que já
        têm conversa no log da instância ou que a consulta de números confirma no WhatsApp (gasta a cota
        de `check-numbers`); as demais voltam em `results` com `not_on_whatsapp` ou `unknown_contact`, sem
        chegar ao WhatsApp. Conta na cota `settings.groups.max_creates_per_day` (padrão 3) e os
        participantes nas cotas de adição (e no intervalo de 10s entre lotes); acima, `429 rate_limited`. A
        cota só volta se a requisição não chegou ao WhatsApp. Com envios suspensos pelo operador,
        `403 sending_suspended`. Com `parent`, o grupo nasce dentro da comunidade.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/CreateGroup" }
            example: { name: "Equipe", participants: ["5511999999999"], description: "Avisos da equipe", settings: { announce: false } }
      responses:
        "201":
          description: Grupo criado e resultado por participante.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/CreateGroupResult" }
        "400": { $ref: "#/components/responses/ValidationFailed" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/InstanceNotFound" }
        "409": { $ref: "#/components/responses/InstanceConflict" }
        "429": { $ref: "#/components/responses/GroupRateLimited" }
        "502": { $ref: "#/components/responses/WorkerError" }
        "504": { $ref: "#/components/responses/WorkerTimeout" }

  /v1/instances/{id}/groups/{jid}:
    parameters: [{ $ref: "#/components/parameters/InstanceID" }, { $ref: "#/components/parameters/GroupJID" }]
    get:
      tags: [Grupos]
      operationId: getGroup
      x-litezap-milestone: M8
      x-zapi: "Metadata do grupo"
      summary: Metadados do grupo
      x-litezap-scope: instancia
      description: Nome, descrição, dono, configurações e participantes (com `admin` e `super_admin`). Participantes vêm com o JID de telefone quando conhecido e o `lid`.
      responses:
        "200":
          description: Grupo.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Group" }
        "400": { $ref: "#/components/responses/ValidationFailed" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/GroupForbidden" }
        "404": { $ref: "#/components/responses/GroupNotFound" }
        "409": { $ref: "#/components/responses/InstanceConflict" }
        "502": { $ref: "#/components/responses/WorkerError" }
    patch:
      tags: [Grupos]
      operationId: updateGroup
      x-litezap-milestone: M8
      x-zapi: "Atualizar nome / descrição / configurações do grupo"
      x-litezap-events: [group.updated]
      summary: Alterar grupo
      x-litezap-scope: instancia
      description: |
        Muda nome, descrição (`""` remove) e configurações, nessa ordem; para na primeira falha (as
        anteriores ficam). As configurações podem vir soltas ou dentro de `settings`. Responde com o grupo
        depois das mudanças. Com o grupo `locked`, só admins mudam os dados.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/UpdateGroup" }
            example: { name: "Novo nome", settings: { locked: true, ephemeral_seconds: 86400 } }
      responses:
        "200":
          description: Grupo depois das mudanças.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Group" }
        "400": { $ref: "#/components/responses/ValidationFailed" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/GroupForbidden" }
        "404": { $ref: "#/components/responses/GroupNotFound" }
        "409": { $ref: "#/components/responses/InstanceConflict" }
        "502": { $ref: "#/components/responses/WorkerError" }

  /v1/instances/{id}/groups/{jid}/photo:
    parameters: [{ $ref: "#/components/parameters/InstanceID" }, { $ref: "#/components/parameters/GroupJID" }]
    put:
      tags: [Grupos]
      operationId: setGroupPhoto
      x-litezap-milestone: M8
      x-zapi: "Atualizar imagem do grupo"
      x-litezap-events: [group.updated]
      summary: Trocar foto do grupo
      x-litezap-scope: instancia
      description: JPEG, PNG ou GIF até 10MB, em `base64` (aceita `data:` URI) ou `url` pública (o servidor baixa, sem endereços privados); recortado no centro e enviado como JPEG 640x640 (WebP não).
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                base64: { type: string }
                url: { type: string, format: uri }
            example: { url: "https://exemplo.com/foto.jpg" }
      responses:
        "204": { description: Foto trocada. }
        "400": { $ref: "#/components/responses/ValidationFailed" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/GroupForbidden" }
        "404": { $ref: "#/components/responses/GroupNotFound" }
        "409": { $ref: "#/components/responses/InstanceConflict" }
        "413": { $ref: "#/components/responses/MediaTooLarge" }
        "502": { $ref: "#/components/responses/WorkerError" }
    delete:
      tags: [Grupos]
      operationId: deleteGroupPhoto
      x-litezap-milestone: M8
      x-zapi: "Atualizar imagem do grupo"
      summary: Remover foto do grupo
      x-litezap-scope: instancia
      description: Remove a foto do grupo.
      responses:
        "204": { description: Foto removida. }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/GroupForbidden" }
        "404": { $ref: "#/components/responses/GroupNotFound" }
        "409": { $ref: "#/components/responses/InstanceConflict" }
        "502": { $ref: "#/components/responses/WorkerError" }

  /v1/instances/{id}/groups/{jid}/participants:
    parameters: [{ $ref: "#/components/parameters/InstanceID" }, { $ref: "#/components/parameters/GroupJID" }]
    post:
      tags: [Grupos]
      operationId: groupParticipants
      x-litezap-milestone: M8
      x-zapi: "Adicionar / remover participantes; promover / remover admin"
      x-litezap-events: [group.updated]
      x-litezap-notes: "Adições: só contatos conhecidos, 10 por lote, 10s entre lotes, cotas por hora e por dia."
      summary: Gerenciar participantes
      x-litezap-scope: instancia
      description: |
        `action`: `add`, `remove`, `promote` ou `demote` (a instância precisa ser admin). Responde um
        resultado por participante pedido. Para `add`: até 10 por requisição; só pessoas com conversa no log
        ou confirmadas pela consulta de números (`@lid` sem conversa é recusado); 10 segundos entre lotes e
        cotas `max_adds_per_hour` (padrão 10) e `max_adds_per_day` (padrão 50) por instância, persistidas;
        acima, `429 rate_limited`; recusado com envios suspensos (`403 sending_suspended`). Demais ações: até
        50 por requisição.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [action, participants]
              properties:
                action: { type: string, enum: [add, remove, promote, demote] }
                participants: { type: array, minItems: 1, maxItems: 50, items: { type: string } }
            example: { action: promote, participants: ["5511999999999"] }
      responses:
        "200":
          description: Resultado por participante.
          content:
            application/json:
              schema:
                type: object
                properties:
                  results: { type: array, items: { $ref: "#/components/schemas/ParticipantResult" } }
              example:
                results:
                  - { participant: "5511999999999@s.whatsapp.net", jid: "5511999999999@s.whatsapp.net", status: ok }
                  - { participant: "5511888888888@s.whatsapp.net", status: privacy_invite_required, code: 403, invite_code: "AbCd" }
        "400": { $ref: "#/components/responses/ValidationFailed" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/GroupForbidden" }
        "404": { $ref: "#/components/responses/GroupNotFound" }
        "409": { $ref: "#/components/responses/InstanceConflict" }
        "429": { $ref: "#/components/responses/GroupRateLimited" }
        "502": { $ref: "#/components/responses/WorkerError" }

  /v1/instances/{id}/groups/{jid}/requests:
    parameters: [{ $ref: "#/components/parameters/InstanceID" }, { $ref: "#/components/parameters/GroupJID" }]
    get:
      tags: [Grupos]
      operationId: listJoinRequests
      x-litezap-milestone: M8
      x-zapi: "Listar solicitações"
      summary: Listar pedidos de entrada
      x-litezap-scope: instancia
      description: Pedidos pendentes em grupos com `approval_required` (só admins).
      responses:
        "200":
          description: Pedidos.
          content:
            application/json:
              schema:
                type: object
                properties:
                  requests:
                    type: array
                    items:
                      type: object
                      properties:
                        jid: { type: string }
                        lid: { type: string }
                        requested_at: { type: string, format: date-time }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/GroupForbidden" }
        "404": { $ref: "#/components/responses/GroupNotFound" }
        "409": { $ref: "#/components/responses/InstanceConflict" }
        "502": { $ref: "#/components/responses/WorkerError" }
    post:
      tags: [Grupos]
      operationId: decideJoinRequests
      x-litezap-milestone: M8
      x-zapi: "Aceitar / recusar solicitações"
      x-litezap-events: [group.updated]
      summary: Aprovar ou rejeitar pedidos
      x-litezap-scope: instancia
      description: Aprova ou rejeita pedidos de entrada (até 50), com resultado por participante.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [action, participants]
              properties:
                action: { type: string, enum: [approve, reject] }
                participants: { type: array, minItems: 1, maxItems: 50, items: { type: string } }
            example: { action: approve, participants: ["5511999999999"] }
      responses:
        "200":
          description: Resultado por participante.
          content:
            application/json:
              schema:
                type: object
                properties:
                  results: { type: array, items: { $ref: "#/components/schemas/ParticipantResult" } }
        "400": { $ref: "#/components/responses/ValidationFailed" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/GroupForbidden" }
        "404": { $ref: "#/components/responses/GroupNotFound" }
        "409": { $ref: "#/components/responses/InstanceConflict" }
        "502": { $ref: "#/components/responses/WorkerError" }

  /v1/instances/{id}/groups/{jid}/invite:
    parameters: [{ $ref: "#/components/parameters/InstanceID" }, { $ref: "#/components/parameters/GroupJID" }]
    get:
      tags: [Grupos]
      operationId: groupInviteLink
      x-litezap-milestone: M8
      x-zapi: "Obter link de convite"
      summary: Link de convite
      x-litezap-scope: instancia
      description: Link `https://chat.whatsapp.com/…` do grupo (a instância precisa ser admin). Quem tem o link entra; a resposta não é cacheável.
      responses:
        "200":
          description: Link.
          content:
            application/json:
              schema: { type: object, properties: { link: { type: string } } }
              example: { link: "https://chat.whatsapp.com/AbCdEfGhIjK" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/GroupForbidden" }
        "404": { $ref: "#/components/responses/GroupNotFound" }
        "409": { $ref: "#/components/responses/InstanceConflict" }
        "502": { $ref: "#/components/responses/WorkerError" }

  /v1/instances/{id}/groups/{jid}/invite/reset:
    parameters: [{ $ref: "#/components/parameters/InstanceID" }, { $ref: "#/components/parameters/GroupJID" }]
    post:
      tags: [Grupos]
      operationId: resetGroupInviteLink
      x-litezap-milestone: M8
      x-zapi: "Redefinir link de convite"
      x-litezap-events: [group.updated]
      summary: Redefinir link de convite
      x-litezap-scope: instancia
      description: Revoga o link atual (para de funcionar para todos) e devolve um novo.
      responses:
        "200":
          description: Link novo.
          content:
            application/json:
              schema: { type: object, properties: { link: { type: string } } }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/GroupForbidden" }
        "404": { $ref: "#/components/responses/GroupNotFound" }
        "409": { $ref: "#/components/responses/InstanceConflict" }
        "502": { $ref: "#/components/responses/WorkerError" }

  /v1/instances/{id}/groups/{jid}/leave:
    parameters: [{ $ref: "#/components/parameters/InstanceID" }, { $ref: "#/components/parameters/GroupJID" }]
    post:
      tags: [Grupos]
      operationId: leaveGroup
      x-litezap-milestone: M8
      x-zapi: "Sair do grupo"
      summary: Sair do grupo
      x-litezap-scope: instancia
      description: O número sai do grupo; para voltar, precisa de convite.
      responses:
        "204": { description: Saiu. }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/GroupForbidden" }
        "404": { $ref: "#/components/responses/GroupNotFound" }
        "409": { $ref: "#/components/responses/InstanceConflict" }
        "502": { $ref: "#/components/responses/WorkerError" }

  /v1/instances/{id}/groups/preview:
    parameters: [{ $ref: "#/components/parameters/InstanceID" }]
    post:
      tags: [Grupos]
      operationId: previewGroupInvite
      x-litezap-milestone: M8
      x-zapi: "Metadata do grupo pelo convite"
      summary: Ver grupo pelo convite
      x-litezap-scope: instancia
      description: Metadados do grupo de um link de convite, sem entrar (`410 invite_revoked`, `400 invite_invalid`).
      requestBody:
        required: true
        content:
          application/json:
            schema: { type: object, required: [code], properties: { code: { type: string, description: "Código ou link https://chat.whatsapp.com/…" } } }
            example: { code: "https://chat.whatsapp.com/AbCdEfGhIjK" }
      responses:
        "200":
          description: Grupo (sem participantes).
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Group" }
        "400": { $ref: "#/components/responses/ValidationFailed" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "409": { $ref: "#/components/responses/InstanceConflict" }
        "410": { $ref: "#/components/responses/InviteRevoked" }
        "502": { $ref: "#/components/responses/WorkerError" }

  /v1/instances/{id}/groups/join:
    parameters: [{ $ref: "#/components/parameters/InstanceID" }]
    post:
      tags: [Grupos]
      operationId: joinGroupInvite
      x-litezap-milestone: M8
      x-zapi: "Aceitar convite do grupo"
      x-litezap-events: [group.updated]
      summary: Entrar por convite
      x-litezap-scope: instancia
      description: |
        Entra no grupo do link. Se o grupo exige aprovação, um pedido é enviado (`pending: true`). Conta na
        cota `settings.groups.max_joins_per_day` (padrão 10); recusado com envios suspensos.
      requestBody:
        required: true
        content:
          application/json:
            schema: { type: object, required: [code], properties: { code: { type: string } } }
            example: { code: "AbCdEfGhIjK" }
      responses:
        "200":
          description: Grupo.
          content:
            application/json:
              schema: { type: object, properties: { jid: { type: string }, pending: { type: boolean } } }
              example: { jid: "120363000000000001@g.us", pending: false }
        "400": { $ref: "#/components/responses/ValidationFailed" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "409": { $ref: "#/components/responses/InstanceConflict" }
        "410": { $ref: "#/components/responses/InviteRevoked" }
        "429": { $ref: "#/components/responses/GroupRateLimited" }
        "502": { $ref: "#/components/responses/WorkerError" }

  /v1/instances/{id}/communities:
    parameters: [{ $ref: "#/components/parameters/InstanceID" }]
    get:
      tags: [Comunidades]
      operationId: listCommunities
      x-litezap-milestone: M8
      x-zapi: "Listar comunidades"
      summary: Listar comunidades
      x-litezap-scope: instancia
      description: Comunidades em que o número está.
      responses:
        "200":
          description: Comunidades.
          content:
            application/json:
              schema:
                type: object
                properties:
                  communities: { type: array, items: { $ref: "#/components/schemas/Group" } }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/InstanceNotFound" }
        "409": { $ref: "#/components/responses/InstanceConflict" }
        "502": { $ref: "#/components/responses/WorkerError" }
    post:
      tags: [Comunidades]
      operationId: createCommunity
      x-litezap-milestone: M8
      x-zapi: "Criar comunidade"
      summary: Criar comunidade
      x-litezap-scope: instancia
      description: "Cria uma comunidade (o WhatsApp cria o grupo de avisos sozinho). Mesmo corpo e mesmas cotas de `POST /groups`, sem `parent` e sem `participants`; a descrição vai na criação (sem ela, vai o nome). Recusa do WhatsApp (contas que não podem criar comunidades): `409 community_not_allowed`."
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/CreateGroup" }
            example: { name: "Condomínio", description: "Comunidade do prédio" }
      responses:
        "201":
          description: Comunidade criada.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/CreateGroupResult" }
        "400": { $ref: "#/components/responses/ValidationFailed" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "409": { $ref: "#/components/responses/InstanceConflict" }
        "429": { $ref: "#/components/responses/GroupRateLimited" }
        "502": { $ref: "#/components/responses/WorkerError" }

  /v1/instances/{id}/communities/{jid}:
    parameters: [{ $ref: "#/components/parameters/InstanceID" }, { $ref: "#/components/parameters/GroupJID" }]
    get:
      tags: [Comunidades]
      operationId: getCommunity
      x-litezap-milestone: M8
      x-zapi: "Metadata da comunidade"
      summary: Metadados da comunidade
      x-litezap-scope: instancia
      description: A comunidade (como um grupo) e seus grupos; o de avisos vem com `announcement_group`. Um grupo comum responde `404 group_not_found`.
      responses:
        "200":
          description: Comunidade e grupos.
          content:
            application/json:
              schema:
                type: object
                properties:
                  community: { $ref: "#/components/schemas/Group" }
                  groups: { type: array, items: { $ref: "#/components/schemas/SubGroup" } }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/GroupForbidden" }
        "404": { $ref: "#/components/responses/GroupNotFound" }
        "409": { $ref: "#/components/responses/InstanceConflict" }
        "502": { $ref: "#/components/responses/WorkerError" }

  /v1/instances/{id}/communities/{jid}/groups:
    parameters: [{ $ref: "#/components/parameters/InstanceID" }, { $ref: "#/components/parameters/GroupJID" }]
    get:
      tags: [Comunidades]
      operationId: listCommunityGroups
      x-litezap-milestone: M8
      x-zapi: "Metadata da comunidade"
      summary: Grupos da comunidade
      x-litezap-scope: instancia
      description: Grupos vinculados à comunidade.
      responses:
        "200":
          description: Grupos.
          content:
            application/json:
              schema:
                type: object
                properties:
                  groups: { type: array, items: { $ref: "#/components/schemas/SubGroup" } }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/GroupForbidden" }
        "404": { $ref: "#/components/responses/GroupNotFound" }
        "409": { $ref: "#/components/responses/InstanceConflict" }
        "502": { $ref: "#/components/responses/WorkerError" }
    post:
      tags: [Comunidades]
      operationId: linkCommunityGroup
      x-litezap-milestone: M8
      x-zapi: "Vincular grupos"
      x-litezap-events: [group.updated]
      summary: Vincular grupo
      x-litezap-scope: instancia
      description: Vincula um grupo existente (em que o número é admin) à comunidade.
      requestBody:
        required: true
        content:
          application/json:
            schema: { type: object, required: [group], properties: { group: { type: string } } }
            example: { group: "120363000000000001@g.us" }
      responses:
        "204": { description: Vinculado. }
        "400": { $ref: "#/components/responses/ValidationFailed" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/GroupForbidden" }
        "404": { $ref: "#/components/responses/GroupNotFound" }
        "409": { $ref: "#/components/responses/InstanceConflict" }
        "502": { $ref: "#/components/responses/WorkerError" }

  /v1/instances/{id}/communities/{jid}/groups/{group}:
    parameters:
      - { $ref: "#/components/parameters/InstanceID" }
      - { $ref: "#/components/parameters/GroupJID" }
      - { name: group, in: path, required: true, description: JID do grupo a desvincular., schema: { type: string } }
    delete:
      tags: [Comunidades]
      operationId: unlinkCommunityGroup
      x-litezap-milestone: M8
      x-zapi: "Desvincular grupos"
      x-litezap-events: [group.updated]
      summary: Desvincular grupo
      x-litezap-scope: instancia
      description: Tira o grupo da comunidade (o grupo continua existindo).
      responses:
        "204": { description: Desvinculado. }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/GroupForbidden" }
        "404": { $ref: "#/components/responses/GroupNotFound" }
        "409": { $ref: "#/components/responses/InstanceConflict" }
        "502": { $ref: "#/components/responses/WorkerError" }

  /v1/instances/{id}/events:
    parameters: [{ $ref: "#/components/parameters/InstanceID" }]
    get:
      tags: [Eventos em tempo real]
      operationId: instanceEvents
      x-litezap-sample: { sse: true, query: { types: "message.*,instance.status" } }
      x-litezap-milestone: M3
      x-zapi: "(sem equivalente)"
      x-litezap-notes: "Sem replay: para não perder eventos, use webhooks."
      summary: Stream da instância
      x-litezap-scope: instancia
      description: |
        Server-Sent Events com os eventos de uma instância. Cada evento sai como `id`, `event` (tipo) e
        `data` (o envelope JSON). A cada 15 segundos vai um comentário `: ping`. Não há replay
        (`Last-Event-ID` é ignorado). Um cliente mais de 256 eventos atrasado recebe `event: error`
        (`slow_consumer`) e é desconectado: reconecte. O token é checado só na conexão.
      parameters:
        - $ref: "#/components/parameters/EventTypes"
      responses:
        "200":
          description: Stream `text/event-stream`, aberto até o cliente sair.
          content:
            text/event-stream:
              schema: { type: string }
              example: |
                retry: 3000
                : connected

                id: evt_5f2c9a1b7d3e4f60
                event: message.received
                data: {"id":"evt_5f2c9a1b7d3e4f60","type":"message.received","version":1,"instance_id":"inst_7f3a9c2e5b1d4086","timestamp":"2026-10-17T13:45:00.123Z","data":{"id":"msg_e29d4b8a0f6c1357","chat":"5511999999999@s.whatsapp.net","from_me":false,"source":"contact","type":"text","text":"Oi!","timestamp":"2026-10-17T13:45:00Z"}}

                : ping
        "400": { $ref: "#/components/responses/ValidationFailed" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/InstanceNotFound" }
  /v1/events:
    get:
      tags: [Eventos em tempo real]
      operationId: allEvents
      x-litezap-sample: { sse: true }
      x-litezap-milestone: M3
      x-zapi: "(sem equivalente)"
      x-litezap-notes: "Sem replay: para não perder eventos, use webhooks."
      summary: Stream global
      x-litezap-scope: admin
      description: Server-Sent Events de todas as instâncias (só token admin). Mesmo formato do stream da instância; use `instance_id` do envelope para separar.
      parameters:
        - $ref: "#/components/parameters/EventTypes"
      responses:
        "200":
          description: Stream `text/event-stream`.
          content:
            text/event-stream:
              schema: { type: string }
              example: |
                retry: 3000
                : connected

                id: evt_0a7d3c9e1f5b2486
                event: instance.status
                data: {"id":"evt_0a7d3c9e1f5b2486","type":"instance.status","version":1,"instance_id":"inst_7f3a9c2e5b1d4086","timestamp":"2026-10-17T13:40:02.511Z","data":{"state":"connected"}}
        "400": { $ref: "#/components/responses/ValidationFailed" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }

  /mcp:
    post:
      tags: [MCP]
      operationId: mcpEndpoint
      x-litezap-milestone: M4
      x-zapi: "MCP Server"
      x-litezap-sample: { headers: { Accept: "application/json, text/event-stream" } }
      summary: Endpoint MCP
      x-litezap-scope: instancia
      description: |
        Endpoint MCP (streamable HTTP, *stateless*: sem `Mcp-Session-Id`, cada requisição é autenticada de novo)
        com o mesmo `Authorization: Bearer` da API. Com token de instância, as ferramentas operam só naquela
        instância. O corpo é uma mensagem JSON-RPC do protocolo MCP (`initialize`, `tools/list`,
        `tools/call`...); o cliente precisa aceitar `application/json` e `text/event-stream`.

        Num `listen_addr` de loopback, pedidos com `Host` que não é loopback são recusados (proteção contra DNS
        rebinding), exceto o host de `public_url`, para um proxy reverso na mesma máquina. Clientes na nuvem
        (ChatGPT, OpenAI) precisam do servidor publicado com TLS. Para clientes locais que iniciam um processo,
        use `litezap mcp` (stdio).
      x-litezap-notes: "Todo envio feito pelo assistente passa pela mesma fila (ritmo, rampa, janela). Prefira clientes que pedem aprovação antes de enviar: mensagens de terceiros podem conter prompt injection."
      requestBody:
        required: true
        content:
          application/json:
            schema: { type: object, description: Mensagem JSON-RPC 2.0 do protocolo MCP. }
            example: { jsonrpc: "2.0", id: 1, method: "tools/list" }
      responses:
        "200":
          description: Resposta JSON-RPC (a lista de ferramentas, no exemplo).
          content:
            application/json:
              example: { jsonrpc: "2.0", id: 1, result: { tools: [ { name: list_instances, description: "..." }, { name: send_message, description: "..." } ] } }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }

  /v1/me:
    get:
      tags: [Operação]
      operationId: me
      x-litezap-milestone: M4
      x-zapi: "(sem equivalente)"
      summary: Identidade do token
      x-litezap-scope: qualquer
      description: Diz quem é o token que fez a chamada. `admin` é `true` para o token admin; com token de instância vem também o `instance_id`. Útil para clientes descobrirem o próprio escopo (o `litezap mcp` usa esta rota).
      responses:
        "200":
          description: Escopo do token.
          content:
            application/json:
              schema:
                type: object
                properties:
                  admin: { type: boolean }
                  role: { type: string, enum: [operator, admin, instance] }
                  instance_id: { type: string, description: Só com token de instância ou chave de API. }
                  key_id: { type: string, description: "Só com chave de API (marco 11)." }
                  scopes: { type: array, items: { type: string }, description: "Só com chave de API." }
              examples:
                instancia:
                  summary: Token de instância
                  value: { admin: false, instance_id: inst_7f3a9c2e5b1d4086 }
                admin:
                  summary: Token admin
                  value: { admin: true }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /healthz:
    get:
      tags: [Operação]
      operationId: healthz
      x-litezap-milestone: M1
      x-zapi: "(sem equivalente)"
      summary: Processo vivo
      x-litezap-scope: operacao
      security: []
      description: Público quando o `listen_addr` é loopback; caso contrário exige o token admin.
      responses:
        "200":
          description: Vivo.
          content:
            application/json:
              example: { status: ok }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
  /readyz:
    get:
      tags: [Operação]
      operationId: readyz
      x-litezap-milestone: M1
      x-zapi: "(sem equivalente)"
      summary: Pronto para atender
      x-litezap-scope: operacao
      security: []
      description: Confere o banco (SQLite). Público quando o `listen_addr` é loopback; caso contrário exige o token admin.
      responses:
        "200":
          description: Pronto.
          content:
            application/json:
              example: { status: ready }
        "503":
          description: Indisponível.
          content:
            application/json:
              example: { status: unavailable }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }

  /v1/instances/bulk:
    post:
      tags: [Plataforma]
      operationId: bulkInstances
      x-litezap-milestone: M11
      x-zapi: "Apagar instâncias em lote (parceiros)"
      x-litezap-events: [instance.status]
      summary: Operação em lote
      x-litezap-scope: admin
      description: |
        Aplica `start`, `stop`, `restart` ou `delete` a várias instâncias, dadas por `ids` ou por `tag`
        (uma das duas), até 100 por requisição. Roda 4 por vez e devolve um resultado por instância: uma
        falha não interrompe as outras. `delete` exige `"confirm": true` e pode levar ~20s por instância
        conectada (logout + parada do worker). Exige o token admin (ou o do operador).
      x-litezap-notes: "delete apaga sessão, mídia, fila e histórico de cada instância. Não há como desfazer."
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [action]
              properties:
                action: { type: string, enum: [start, stop, restart, delete] }
                ids: { type: array, items: { type: string }, maxItems: 100 }
                tag: { type: string }
                confirm: { type: boolean, description: "Obrigatório (`true`) para `delete`." }
            examples:
              porEtiqueta:
                summary: Parar todas as instâncias de uma etiqueta
                value: { action: stop, tag: cliente-acme }
              apagar:
                summary: Apagar por id
                value: { action: delete, ids: [inst_7f3a9c2e5b1d4086, inst_2b9d4f6a8c0e1357], confirm: true }
      responses:
        "200":
          description: Resultado por instância.
          content:
            application/json:
              schema:
                type: object
                properties:
                  action: { type: string }
                  succeeded: { type: integer }
                  failed: { type: integer }
                  results:
                    type: array
                    items:
                      type: object
                      properties:
                        id: { type: string }
                        ok: { type: boolean }
                        error: { type: object, description: "Presente quando ok = false: { code, message, retryable }." }
              example:
                action: delete
                succeeded: 1
                failed: 1
                results:
                  - { id: inst_7f3a9c2e5b1d4086, ok: true }
                  - { id: inst_nao_existe, ok: false, error: { code: instance_not_found, message: instance not found, retryable: false } }
        "400": { $ref: "#/components/responses/ValidationFailed" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }

  /v1/instances/{id}/api-keys:
    parameters:
      - $ref: "#/components/parameters/InstanceID"
    post:
      tags: [Plataforma]
      operationId: createAPIKey
      x-litezap-milestone: M11
      x-zapi: "(sem equivalente)"
      summary: Criar chave de API
      x-litezap-scope: instancia
      description: |
        Cria uma chave (`lzk_...`) presa a esta instância, com escopos:

        - `read`: rotas `GET` de leitura — instância, mensagens, fila, chats, contatos, mídia, grupos,
          comunidades, perfil, privacidade, bloqueados, eventos (SSE), uso, status, canais (e publicações),
          etiquetas e perfil comercial. Nunca QR, configurações, hook de agente, convites de grupo nem
          escritas de status, canal ou etiqueta.
        - `send`: `POST /messages` e `GET /messages/{msg_id}` (status do envio).

        Qualquer outra rota responde `403 forbidden`, inclusive `/mcp`, webhooks, ações de ciclo de vida e
        a gestão de chaves. A chave aparece **uma única vez** nesta resposta (só o hash fica guardado).
        Opcionais: `ip_allowlist` (IPs/CIDRs) e `expires_in_seconds`. Até 50 chaves por instância (as expiradas
        são descartadas). Apagar a instância apaga as chaves.

        Uma chave também respeita a `ip_allowlist` da instância; no stream SSE ela nunca recebe `instance.qr` nem `instance.pair_code`, e um stream aberto é encerrado em até um heartbeat (15s) quando a chave é revogada, expira ou sai da allowlist. Rotacionar o token da instância revoga as chaves criadas com ele (as criadas pelo admin continuam).
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name, scopes]
              properties:
                name: { type: string, maxLength: 100 }
                scopes: { type: array, items: { type: string, enum: [read, send] } }
                ip_allowlist: { type: array, items: { type: string } }
                expires_in_seconds: { type: integer, minimum: 0, description: "0 ou ausente: não expira." }
            example: { name: "painel do cliente", scopes: [read], ip_allowlist: ["203.0.113.0/24"], expires_in_seconds: 2592000 }
      responses:
        "201":
          description: Chave criada.
          content:
            application/json:
              schema:
                type: object
                properties:
                  api_key: { $ref: "#/components/schemas/APIKey" }
                  key: { type: string, description: "A chave (`lzk_...`). Mostrada só uma vez." }
              example:
                api_key: { id: key_5c1e9a7d3b2f4086, instance_id: inst_7f3a9c2e5b1d4086, name: painel do cliente, scopes: [read], ip_allowlist: ["203.0.113.0/24"], created_at: "2026-10-18T09:00:00Z", expires_at: "2026-11-17T09:00:00Z" }
                key: lzk_RXhlbXBsby1uYW8tdXNlLWVzdGEtY2hhdmU
        "400": { $ref: "#/components/responses/ValidationFailed" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/InstanceNotFound" }
    get:
      tags: [Plataforma]
      operationId: listAPIKeys
      x-litezap-milestone: M11
      x-zapi: "(sem equivalente)"
      summary: Listar chaves de API
      x-litezap-scope: instancia
      responses:
        "200":
          description: As chaves da instância (sem o segredo).
          content:
            application/json:
              schema:
                type: object
                properties:
                  api_keys: { type: array, items: { $ref: "#/components/schemas/APIKey" } }
              example:
                api_keys:
                  - { id: key_5c1e9a7d3b2f4086, instance_id: inst_7f3a9c2e5b1d4086, name: painel do cliente, scopes: [read], ip_allowlist: [], created_at: "2026-10-18T09:00:00Z", last_used_at: "2026-10-18T09:12:00Z" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/InstanceNotFound" }

  /v1/instances/{id}/api-keys/{key_id}:
    parameters:
      - $ref: "#/components/parameters/InstanceID"
      - { name: key_id, in: path, required: true, description: "Id da chave (`key_...`).", schema: { type: string } }
    delete:
      tags: [Plataforma]
      operationId: deleteAPIKey
      x-litezap-milestone: M11
      x-zapi: "(sem equivalente)"
      summary: Revogar chave de API
      x-litezap-scope: instancia
      description: Apaga a chave; a próxima chamada com ela recebe `401 unauthorized`.
      responses:
        "204": { description: Revogada. }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }

  /v1/instances/{id}/pairing-links:
    parameters:
      - $ref: "#/components/parameters/InstanceID"
    post:
      tags: [Plataforma]
      operationId: createPairingLink
      x-litezap-milestone: M11
      x-zapi: "SDK de conexão / Gerar token do SDK"
      x-litezap-events: [instance.paired]
      summary: Gerar link de pareamento
      x-litezap-scope: admin
      description: |
        Gera um link para uma página hospedada pelo próprio servidor (`/pair/`) que mostra o QR ao vivo e
        permite parear pelo número de telefone — sem expor o token da instância. O link:

        - é assinado (HMAC-SHA256 com a chave `data/pairing_key`) e registrado no banco;
        - vale `ttl_seconds` (60 a 600; padrão 600) **ou** até o primeiro pareamento concluído;
        - leva o token no fragmento (`#t=...`), que o navegador nunca envia ao servidor (não vai para logs
          de acesso nem para `Referer`); a página o tira da barra de endereço ao abrir;
        - pode ser embutido num iframe só nas origens de `pairing_frame_ancestors` (CSP `frame-ancestors`;
          vazio = não pode ser embutido). Há um web component pronto: `<script src=".../pair/embed.js">` e
          `<litezap-pair url="...">`.

        A base do link é `public_url` (ou o `listen_addr`). Exige o token admin (ou o do operador); até 20
        links vivos por instância. Quando o pareamento termina sai o evento `instance.paired` com `via_link: true`.
      x-litezap-notes: "Quem tiver o link pode parear a instância com o próprio número enquanto ele valer. Envie só ao cliente e revogue se vazar."
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                ttl_seconds: { type: integer, minimum: 60, maximum: 600, default: 600 }
            example: { ttl_seconds: 600 }
      responses:
        "201":
          description: Link gerado.
          content:
            application/json:
              schema:
                type: object
                properties:
                  id: { type: string }
                  url: { type: string, description: "Página de pareamento com o token no fragmento." }
                  token: { type: string, description: "O token do link (o mesmo do fragmento)." }
                  expires_at: { type: string, format: date-time }
              example:
                id: wEeHr7odSNU0pM_Rb4O5WQ
                url: "https://zap.seu-dominio.com/pair/#t=pl1.wEeHr7odSNU0pM_Rb4O5WQ.1792342931.Yzlm6Nb6zRKq7pSI0XVQSA1OCyKl6le1r4ca4qL63wY"
                token: pl1.wEeHr7odSNU0pM_Rb4O5WQ.1792342931.Yzlm6Nb6zRKq7pSI0XVQSA1OCyKl6le1r4ca4qL63wY
                expires_at: "2026-10-18T17:02:11Z"
        "400": { $ref: "#/components/responses/ValidationFailed" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/InstanceNotFound" }
    delete:
      tags: [Plataforma]
      operationId: revokePairingLinks
      x-litezap-milestone: M11
      x-zapi: "(sem equivalente)"
      summary: Revogar links de pareamento
      x-litezap-scope: admin
      description: Revoga todos os links ainda não usados da instância; a página aberta passa a mostrar "link expirado".
      responses:
        "200":
          description: Quantos links foram revogados.
          content:
            application/json:
              example: { revoked: 1 }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/InstanceNotFound" }

  /v1/instances/{id}/usage:
    parameters:
      - $ref: "#/components/parameters/InstanceID"
    get:
      tags: [Plataforma]
      operationId: instanceUsage
      x-litezap-milestone: M11
      x-zapi: "(sem equivalente)"
      summary: Uso da instância
      x-litezap-scope: instancia
      description: |
        Contadores diários (dias UTC) da instância: mensagens enviadas pela API e recebidas, bytes de mídia,
        segundos conectada e entregas de webhook. `from` e `to` (`YYYY-MM-DD`) padrão: últimos 31 dias;
        intervalo máximo de 366 dias. Os números são os mesmos do `GET /v1/usage` do operador.
      parameters:
        - { name: from, in: query, schema: { type: string, format: date } }
        - { name: to, in: query, schema: { type: string, format: date } }
      responses:
        "200":
          description: Uso por dia.
          content:
            application/json:
              example:
                instance_id: inst_7f3a9c2e5b1d4086
                from: "2026-10-01"
                to: "2026-10-18"
                usage:
                  - { instance_id: inst_7f3a9c2e5b1d4086, day: "2026-10-17", messages_sent: 120, messages_received: 340, media_bytes: 1048576, connected_seconds: 86400, webhook_deliveries: 460 }
        "400": { $ref: "#/components/responses/ValidationFailed" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/InstanceNotFound" }

  /v1/instances/{id}/status:
    parameters: [{ $ref: "#/components/parameters/InstanceID" }]
    post:
      tags: [Status]
      operationId: postStatus
      x-litezap-milestone: M10
      x-zapi: "Enviar status de texto / imagem / vídeo"
      x-litezap-events: [message.sent, message.status]
      x-litezap-notes: "Vai para todos os contatos permitidos pela privacidade do status: fila de envio e no máximo 10 por dia."
      summary: Publicar status
      x-litezap-scope: instancia
      description: |
        Publica um status (story) de texto, imagem ou vídeo. Os destinatários são os da privacidade do
        status configurada no celular (veja `GET /status/privacy`); o WhatsApp não permite escolher por
        envio. Passa pela fila de envio como `POST /messages` (ritmo, janela de horário, rampa,
        suspensão do operador, `client_message_id`, `?wait=true`) e tem teto de 10 status por dia por
        instância (`429 rate_limited`). Texto: até 700 caracteres, `background_color`/`text_color`
        `#RRGGBB` e `font` 0, 1, 2, 6, 7, 8, 9 ou 10.
      parameters:
        - { name: wait, in: query, schema: { type: boolean }, description: "Espera o envio (até `timeout`, padrão 30s)." }
        - { name: timeout, in: query, schema: { type: string, example: 30s } }
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/StatusInput" }
            examples:
              texto: { summary: Texto, value: { type: text, text: "Bom dia! Abrimos às 9h.", background_color: "#1E6E4F", font: 1 } }
              imagem: { summary: Imagem, value: { type: image, caption: "Novidade da semana", media: { url: "https://exemplo.com/foto.jpg" } } }
      responses:
        "200":
          description: Publicado (com `?wait=true`).
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Message" }
              example: { id: msg_5c1e9a7d3b2f4068, wa_message_id: 3EB0C4D5E6F7A8B9C0D1E2, chat: status@broadcast, from_me: true, source: api, type: text, text: "Bom dia! Abrimos às 9h.", status: sent, timestamp: "2026-10-18T12:00:03Z" }
        "202":
          description: Na fila.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Message" }
        "400": { $ref: "#/components/responses/ValidationFailed" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/InstanceNotFound" }
        "409": { $ref: "#/components/responses/SendConflict" }
        "413": { $ref: "#/components/responses/MediaTooLarge" }
        "422": { $ref: "#/components/responses/MediaFetchFailed" }
        "429": { $ref: "#/components/responses/SocialRateLimited" }
    get:
      tags: [Status]
      operationId: listStatus
      x-litezap-milestone: M10
      x-zapi: "(sem equivalente)"
      x-litezap-events: [status.received]
      summary: Status recentes
      x-litezap-scope: instancia
      description: |
        Status no log, do mais recente para o mais antigo: os dos contatos recebidos com a instância
        conectada (`sender` = autor; mídia baixável por `media.url`) e os nossos. Ficam no chat
        `status@broadcast`, que não aparece em `GET /chats`.
      parameters:
        - $ref: "#/components/parameters/Limit"
        - $ref: "#/components/parameters/Cursor"
      responses:
        "200":
          description: Uma página de status.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/MessagePage" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/InstanceNotFound" }

  /v1/instances/{id}/status/privacy:
    parameters: [{ $ref: "#/components/parameters/InstanceID" }]
    get:
      tags: [Status]
      operationId: statusPrivacy
      x-litezap-milestone: M10
      x-zapi: "(sem equivalente)"
      summary: Privacidade do status
      x-litezap-scope: instancia
      description: Quem recebe os status do número, como configurado no celular (Configurações → Privacidade → Status).
      responses:
        "200":
          description: Privacidade do status.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/StatusPrivacy" }
              example: { type: blacklist, list: ["5511988887777@s.whatsapp.net"] }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/InstanceNotFound" }
        "409": { $ref: "#/components/responses/InstanceConflict" }
        "502": { $ref: "#/components/responses/WorkerError" }

  /v1/instances/{id}/status/{msg_id}:
    parameters:
      - $ref: "#/components/parameters/InstanceID"
      - { name: msg_id, in: path, required: true, schema: { type: string }, description: "Id do LiteZap (ou wa_message_id) do status." }
    delete:
      tags: [Status]
      operationId: deleteStatus
      x-litezap-milestone: M10
      x-zapi: "(sem equivalente)"
      summary: Apagar status
      x-litezap-scope: instancia
      description: Apaga para todos um status nosso (pela fila, como um `revoke`). Aceita `?wait=true`.
      responses:
        "200": { description: Apagado., content: { application/json: { schema: { $ref: "#/components/schemas/Message" } } } }
        "202": { description: Na fila., content: { application/json: { schema: { $ref: "#/components/schemas/Message" } } } }
        "400": { $ref: "#/components/responses/ValidationFailed" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }

  /v1/instances/{id}/status/{msg_id}/reply:
    parameters:
      - $ref: "#/components/parameters/InstanceID"
      - { name: msg_id, in: path, required: true, schema: { type: string }, description: "Id do LiteZap (ou wa_message_id) do status do contato." }
    post:
      tags: [Status]
      operationId: replyStatus
      x-litezap-milestone: M10
      x-zapi: "Responder status com texto"
      x-litezap-events: [message.sent]
      summary: Responder status
      x-litezap-scope: instancia
      description: Manda um texto ao autor de um status recebido, citando o status (como responder pelo app). Vai pela fila; aceita `?wait=true`.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [text]
              properties:
                text: { type: string }
                client_message_id: { type: string }
            example: { text: "Que lindo!" }
      responses:
        "200": { description: Enviada., content: { application/json: { schema: { $ref: "#/components/schemas/Message" } } } }
        "202": { description: Na fila., content: { application/json: { schema: { $ref: "#/components/schemas/Message" } } } }
        "400": { $ref: "#/components/responses/ValidationFailed" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }

  /v1/instances/{id}/channels:
    parameters: [{ $ref: "#/components/parameters/InstanceID" }]
    get:
      tags: [Canais]
      operationId: listChannels
      x-litezap-milestone: M10
      x-zapi: "Listar canais"
      summary: Listar canais seguidos
      x-litezap-scope: instancia
      description: Canais que o número segue ou administra, com o papel (`owner`, `admin`, `subscriber`).
      responses:
        "200":
          description: Canais.
          content:
            application/json:
              schema:
                type: object
                properties:
                  channels: { type: array, items: { $ref: "#/components/schemas/Channel" } }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/InstanceNotFound" }
        "409": { $ref: "#/components/responses/InstanceConflict" }
        "502": { $ref: "#/components/responses/WorkerError" }
    post:
      tags: [Canais]
      operationId: createChannel
      x-litezap-milestone: M10
      x-zapi: "Criar canal"
      summary: Criar canal
      x-litezap-scope: instancia
      description: Cria um canal do número (dono). Foto opcional (JPEG, PNG ou GIF por URL ou base64, recortada em 640x640). No máximo 2 por dia por instância.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/CreateChannel" }
            example: { name: "Minha loja", description: "Ofertas da semana" }
      responses:
        "201": { description: Canal criado., content: { application/json: { schema: { $ref: "#/components/schemas/Channel" } } } }
        "400": { $ref: "#/components/responses/ValidationFailed" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/InstanceNotFound" }
        "429": { $ref: "#/components/responses/SocialRateLimited" }
        "502": { $ref: "#/components/responses/WorkerError" }

  /v1/instances/{id}/channels/preview:
    parameters: [{ $ref: "#/components/parameters/InstanceID" }]
    post:
      tags: [Canais]
      operationId: previewChannel
      x-litezap-milestone: M10
      x-zapi: "Metadata do canal (por convite)"
      summary: Canal por link de convite
      x-litezap-scope: instancia
      description: Lê um canal pelo link (`https://whatsapp.com/channel/...`) ou código de convite, sem segui-lo (`role` vem vazio).
      requestBody:
        required: true
        content:
          application/json:
            schema: { type: object, required: [invite], properties: { invite: { type: string } } }
            example: { invite: "https://whatsapp.com/channel/0029VaExemplo" }
      responses:
        "200": { description: Canal., content: { application/json: { schema: { $ref: "#/components/schemas/Channel" } } } }
        "400": { $ref: "#/components/responses/ValidationFailed" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }

  /v1/instances/{id}/channels/{jid}:
    parameters: [{ $ref: "#/components/parameters/InstanceID" }, { $ref: "#/components/parameters/ChannelJID" }]
    get:
      tags: [Canais]
      operationId: getChannel
      x-litezap-milestone: M10
      x-zapi: "Metadata do canal"
      summary: Metadados do canal
      x-litezap-scope: instancia
      description: Nome, descrição, seguidores, link de convite, verificação e o papel do número no canal.
      responses:
        "200": { description: Canal., content: { application/json: { schema: { $ref: "#/components/schemas/Channel" } } } }
        "400": { $ref: "#/components/responses/ValidationFailed" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }

  /v1/instances/{id}/channels/{jid}/{action}:
    parameters:
      - $ref: "#/components/parameters/InstanceID"
      - $ref: "#/components/parameters/ChannelJID"
      - { name: action, in: path, required: true, schema: { type: string, enum: [follow, unfollow, mute, unmute] } }
    post:
      tags: [Canais]
      operationId: channelAction
      x-litezap-milestone: M10
      x-zapi: "Seguir / deixar de seguir / silenciar / reativar canal"
      summary: Seguir, deixar de seguir, silenciar
      x-litezap-scope: instancia
      description: |
        `follow`, `unfollow`, `mute` ou `unmute` (o celular acompanha). Seguir e deixar de seguir têm
        limite de 3 por minuto, 10 por hora e 30 por dia por instância e são recusados com envios suspensos pelo
        operador.
      responses:
        "204": { description: Feito. }
        "400": { $ref: "#/components/responses/ValidationFailed" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/SocialRateLimited" }

  /v1/instances/{id}/channels/{jid}/messages:
    parameters: [{ $ref: "#/components/parameters/InstanceID" }, { $ref: "#/components/parameters/ChannelJID" }]
    get:
      tags: [Canais]
      operationId: channelMessages
      x-litezap-milestone: M10
      x-zapi: "(sem equivalente)"
      summary: Publicações do canal
      x-litezap-scope: instancia
      description: Últimas publicações (texto, tipo de mídia, visualizações, reações). `before` = `server_id` para paginar para trás.
      parameters:
        - { name: count, in: query, schema: { type: integer, minimum: 1, maximum: 100, default: 20 } }
        - { name: before, in: query, schema: { type: integer } }
      responses:
        "200":
          description: Publicações.
          content:
            application/json:
              schema: { type: object, properties: { messages: { type: array, items: { $ref: "#/components/schemas/ChannelPost" } } } }
        "400": { $ref: "#/components/responses/ValidationFailed" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
    post:
      tags: [Canais]
      operationId: publishChannel
      x-litezap-milestone: M10
      x-zapi: "(via enviar mensagem para o canal)"
      x-litezap-events: [message.sent]
      summary: Publicar no canal
      x-litezap-scope: instancia
      description: |
        Publica texto, imagem, vídeo, áudio ou documento num canal de que o número é dono ou admin
        (`403 forbidden` caso contrário). Vai pela fila de envio; aceita `?wait=true` e `client_message_id`.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/ChannelPostInput" }
            example: { type: text, text: "Promoção relâmpago até 18h!" }
      responses:
        "200": { description: Publicado., content: { application/json: { schema: { $ref: "#/components/schemas/Message" } } } }
        "202": { description: Na fila., content: { application/json: { schema: { $ref: "#/components/schemas/Message" } } } }
        "400": { $ref: "#/components/responses/ValidationFailed" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }

  /v1/instances/{id}/channels/{jid}/reactions:
    parameters: [{ $ref: "#/components/parameters/InstanceID" }, { $ref: "#/components/parameters/ChannelJID" }]
    post:
      tags: [Canais]
      operationId: channelReact
      x-litezap-milestone: M10
      x-zapi: "(sem equivalente)"
      summary: Reagir a publicação
      x-litezap-scope: instancia
      description: Reage a uma publicação pelo `server_id` (`emoji` vazio remove). Limite de 30 por minuto por instância.
      requestBody:
        required: true
        content:
          application/json:
            schema: { type: object, required: [server_id], properties: { server_id: { type: integer }, emoji: { type: string } } }
            example: { server_id: 123, emoji: "👍" }
      responses:
        "204": { description: Feito. }
        "400": { $ref: "#/components/responses/ValidationFailed" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "429": { $ref: "#/components/responses/SocialRateLimited" }

  /v1/instances/{id}/labels:
    parameters: [{ $ref: "#/components/parameters/InstanceID" }]
    get:
      tags: [Etiquetas e Business]
      operationId: listLabels
      x-litezap-milestone: M10
      x-zapi: "Listar etiquetas"
      x-litezap-events: [label.updated]
      summary: Listar etiquetas
      x-litezap-scope: instancia
      description: Etiquetas do WhatsApp Business sincronizadas do celular (inclusive as que existiam ao parear), com as conversas de cada uma.
      responses:
        "200":
          description: Etiquetas.
          content:
            application/json:
              schema: { type: object, properties: { labels: { type: array, items: { $ref: "#/components/schemas/Label" } } } }
              example: { labels: [{ id: "1", name: "Novo cliente", color: 1, chats: ["5511988887777@s.whatsapp.net"] }] }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "409": { $ref: "#/components/responses/BusinessRequired" }
    post:
      tags: [Etiquetas e Business]
      operationId: createLabel
      x-litezap-milestone: M10
      x-zapi: "Criar etiqueta"
      x-litezap-events: [label.updated]
      summary: Criar etiqueta
      x-litezap-scope: instancia
      description: Cria uma etiqueta (o celular recebe pelo app state). `color` é o índice 0–19 da paleta do WhatsApp.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/LabelInput" }
            example: { name: "Pagamento pendente", color: 3 }
      responses:
        "201": { description: Criada., content: { application/json: { schema: { $ref: "#/components/schemas/Label" } } } }
        "400": { $ref: "#/components/responses/ValidationFailed" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "409": { $ref: "#/components/responses/BusinessRequired" }

  /v1/instances/{id}/labels/{label_id}:
    parameters:
      - $ref: "#/components/parameters/InstanceID"
      - { name: label_id, in: path, required: true, schema: { type: string } }
    patch:
      tags: [Etiquetas e Business]
      operationId: updateLabel
      x-litezap-milestone: M10
      x-zapi: "Editar etiqueta"
      x-litezap-events: [label.updated]
      summary: Renomear ou mudar a cor
      x-litezap-scope: instancia
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/LabelInput" }
      responses:
        "200": { description: Etiqueta., content: { application/json: { schema: { $ref: "#/components/schemas/Label" } } } }
        "400": { $ref: "#/components/responses/ValidationFailed" }
        "404": { $ref: "#/components/responses/NotFound" }
        "409": { $ref: "#/components/responses/BusinessRequired" }
    delete:
      tags: [Etiquetas e Business]
      operationId: deleteLabel
      x-litezap-milestone: M10
      x-zapi: "Apagar etiqueta"
      x-litezap-events: [label.updated]
      summary: Apagar etiqueta
      x-litezap-scope: instancia
      description: Apaga a etiqueta (sai de todas as conversas, também no celular).
      responses:
        "204": { description: Apagada. }
        "404": { $ref: "#/components/responses/NotFound" }
        "409": { $ref: "#/components/responses/BusinessRequired" }

  /v1/instances/{id}/labels/{label_id}/chats/{jid}:
    parameters:
      - $ref: "#/components/parameters/InstanceID"
      - { name: label_id, in: path, required: true, schema: { type: string } }
      - $ref: "#/components/parameters/ChatJID"
    put:
      tags: [Etiquetas e Business]
      operationId: labelChat
      x-litezap-milestone: M10
      x-zapi: "Atribuir etiqueta a um chat"
      x-litezap-events: [label.updated]
      summary: Etiquetar conversa
      x-litezap-scope: instancia
      responses:
        "200": { description: A etiqueta com suas conversas., content: { application/json: { schema: { $ref: "#/components/schemas/Label" } } } }
        "404": { $ref: "#/components/responses/NotFound" }
        "409": { $ref: "#/components/responses/BusinessRequired" }
    delete:
      tags: [Etiquetas e Business]
      operationId: unlabelChat
      x-litezap-milestone: M10
      x-zapi: "Remover etiqueta de um chat"
      x-litezap-events: [label.updated]
      summary: Remover etiqueta da conversa
      x-litezap-scope: instancia
      responses:
        "200": { description: A etiqueta com suas conversas., content: { application/json: { schema: { $ref: "#/components/schemas/Label" } } } }
        "404": { $ref: "#/components/responses/NotFound" }
        "409": { $ref: "#/components/responses/BusinessRequired" }

  /v1/instances/{id}/labels/{label_id}/messages/{msg_id}:
    parameters:
      - $ref: "#/components/parameters/InstanceID"
      - { name: label_id, in: path, required: true, schema: { type: string } }
      - { name: msg_id, in: path, required: true, schema: { type: string }, description: Id do LiteZap da mensagem. }
    put:
      tags: [Etiquetas e Business]
      operationId: labelMessage
      x-litezap-milestone: M10
      x-zapi: "(sem equivalente)"
      x-litezap-events: [label.updated]
      summary: Etiquetar mensagem
      x-litezap-scope: instancia
      responses:
        "204": { description: Feito. }
        "404": { $ref: "#/components/responses/NotFound" }
        "409": { $ref: "#/components/responses/BusinessRequired" }
    delete:
      tags: [Etiquetas e Business]
      operationId: unlabelMessage
      x-litezap-milestone: M10
      x-zapi: "(sem equivalente)"
      x-litezap-events: [label.updated]
      summary: Remover etiqueta da mensagem
      x-litezap-scope: instancia
      responses:
        "204": { description: Feito. }
        "404": { $ref: "#/components/responses/NotFound" }
        "409": { $ref: "#/components/responses/BusinessRequired" }

  /v1/instances/{id}/business/profile:
    parameters: [{ $ref: "#/components/parameters/InstanceID" }]
    get:
      tags: [Etiquetas e Business]
      operationId: businessProfile
      x-litezap-milestone: M10
      x-zapi: "Dados da conta business"
      summary: Perfil comercial
      x-litezap-scope: instancia
      description: Endereço, e-mail, categorias, horário de funcionamento e opções (site, descrição) da própria conta Business (somente leitura).
      responses:
        "200":
          description: Perfil comercial.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/BusinessProfile" }
              example: { jid: 5511999999999@s.whatsapp.net, business_name: "Loja Exemplo", address: "Rua A, 10", email: "oi@loja.exemplo", categories: [Varejo], timezone: America/Sao_Paulo, hours: [{ day: mon, mode: open_24h }] }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "409": { $ref: "#/components/responses/BusinessRequired" }

components:
  securitySchemes:
    bearer:
      type: http
      scheme: bearer
      description: "Token admin (`lza_...`, de `data/admin_token` ou `LITEZAP_ADMIN_TOKEN`), token de instância (`lzi_...`) ou chave de API com escopo (`lzk_...`)."
  parameters:
    InstanceID:
      name: id
      in: path
      required: true
      description: "Id da instância (`inst_...`)."
      schema: { type: string }
    ChatJID:
      name: jid
      in: path
      required: true
      description: "JID do chat (`...@s.whatsapp.net`, `...@g.us`, `...@lid`) ou número com DDI. Prefira o `jid` devolvido por `GET /chats` ou `check-numbers`."
      schema: { type: string }
    GroupJID:
      name: jid
      in: path
      required: true
      description: "JID do grupo ou comunidade (`120363…@g.us`)."
      schema: { type: string }
    MediaID:
      name: media_id
      in: path
      required: true
      description: "Id da mídia (`med_...`), de `media.id` no log."
      schema: { type: string }
    Limit:
      name: limit
      in: query
      description: Itens por página. Padrão 50, máximo 200.
      schema: { type: integer, minimum: 1, maximum: 200, default: 50 }
    Cursor:
      name: cursor
      in: query
      description: "`next_cursor` da página anterior (opaco)."
      schema: { type: string }
    WebhookID:
      name: wid
      in: path
      required: true
      description: "Id do webhook (`wh_...`)."
      schema: { type: string }
    EventTypes:
      name: types
      in: query
      description: "Filtro separado por vírgulas: tipos exatos (`instance.status`), famílias (`message.*`) ou `*`. Omitido: todos. `chat.presence`, `status.received` e `channel.message` são opt-in: nem `*` nem a omissão os incluem (use `*,chat.presence,status.received`)."
      schema: { type: string, example: "message.*,instance.status" }

    ChannelJID:
      name: jid
      in: path
      required: true
      description: "JID do canal (`<id>@newsletter`) ou só o id numérico."
      schema: { type: string, example: 120363000000000001@newsletter }

  schemas:
    Error:
      type: object
      required: [error]
      properties:
        error:
          type: object
          required: [code, message, retryable]
          properties:
            code: { type: string, description: Código estável (veja a tabela de erros). }
            message: { type: string, description: Texto para humanos; nunca traz telefone nem token. }
            retryable: { type: boolean, description: Se o cliente pode tentar de novo. }
    InstanceState:
      type: string
      enum: [created, starting, qr_pending, connecting, connected, reconnecting, logged_out, banned, failed, stopped]
    Instance:
      type: object
      required: [id, name, state, created_at, updated_at]
      properties:
        id: { type: string }
        name: { type: string }
        state: { $ref: "#/components/schemas/InstanceState" }
        state_reason: { type: string, description: "Ex.: `proxy_unreachable`, `temp_banned:<duração>`." }
        phone: { type: string, description: Número conectado (depois do pareamento). }
        tags: { type: array, items: { type: string }, description: "Etiquetas (marco 11)." }
        ip_allowlist: { type: array, items: { type: string }, description: "CIDRs de onde o token da instância é aceito; vazio = qualquer (marco 11)." }
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }
    APIKey:
      type: object
      properties:
        id: { type: string, description: "Id da chave (`key_...`)." }
        instance_id: { type: string }
        name: { type: string }
        scopes: { type: array, items: { type: string, enum: [read, send] } }
        ip_allowlist: { type: array, items: { type: string } }
        created_at: { type: string, format: date-time }
        expires_at: { type: string, format: date-time, description: "Ausente: não expira." }
        last_used_at: { type: string, format: date-time, description: "Atualizado no máximo uma vez por minuto." }
    InstanceCreated:
      type: object
      properties:
        instance: { $ref: "#/components/schemas/Instance" }
        token: { type: string, description: "Token da instância (`lzi_...`). Mostrado só uma vez." }
    Ramp:
      type: object
      properties:
        enabled: { type: boolean, default: true }
        start_per_day: { type: integer, default: 50 }
        end_per_day: { type: integer, default: 150 }
        days: { type: integer, default: 14, minimum: 1, maximum: 365 }
    SendWindow:
      type: object
      properties:
        start: { type: string, description: "HH:MM; vazio com end vazio = sem janela.", example: "08:00" }
        end: { type: string, example: "20:00" }
    Settings:
      type: object
      properties:
        min_interval_ms: { type: integer, default: 3000, description: "Intervalo mínimo entre envios. Mínimo 1000 sem unsafe_pacing." }
        jitter_ms: { type: integer, default: 2000, description: Variação aleatória somada ao intervalo. }
        max_per_minute: { type: integer, default: 20, description: "Teto por minuto. Máximo 60 sem unsafe_pacing." }
        typing_simulation: { type: boolean, default: true, description: "Mostra \"digitando\" antes de enviar (~50ms por caractere, até 5s)." }
        ramp: { $ref: "#/components/schemas/Ramp" }
        send_window: { $ref: "#/components/schemas/SendWindow" }
        timezone: { type: string, description: "Fuso IANA (ex.: America/Sao_Paulo); vazio = fuso do servidor." }
        unsafe_pacing: { type: boolean, default: false, description: Remove os pisos de ritmo. Só admin. }
        proxy_url: { type: string, description: "http://, https:// ou socks5://; senha mascarada na leitura." }
        calls: { $ref: "#/components/schemas/CallSettings" }
        presence: { $ref: "#/components/schemas/PresenceSettings" }
        groups: { $ref: "#/components/schemas/GroupLimits" }
        auto_read: { $ref: "#/components/schemas/AutoReadSettings" }
    AutoReadSettings:
      type: object
      description: Leitura automática (marco 9).
      properties:
        enabled: { type: boolean, default: false, description: "Marca as mensagens recebidas como lidas 2 a 5s depois, em lote por chat, no máximo uma confirmação por segundo. Com a confirmação de leitura desligada na privacidade, o contato não vê os tiques azuis." }
        groups: { type: boolean, default: false, description: Também em grupos. }
    CallSettings:
      type: object
      description: Política de chamadas recebidas (marco 7).
      properties:
        auto_reject: { type: boolean, default: false, description: "Rejeita toda chamada individual assim que toca (< 3s); `call.received` sai com `rejected: true`. Chamadas em grupo só são informadas." }
        reject_message:
          type: string
          maxLength: 1000
          description: "Mandada ao autor de chamadas individuais rejeitadas, pela fila (ritmo, janela, rampa); no máximo uma por contato a cada 10 min; expira em 1h. Vazio: sem mensagem."
    PresenceSettings:
      type: object
      description: Presença da própria conta (marco 7).
      properties:
        online:
          type: boolean
          default: false
          description: "Marca a conta como disponível a cada conexão: necessário para `chat.presence` e `presence/subscribe`. Enquanto ligado, o celular deixa de mostrar notificações."
    WebhookFilters:
      type: object
      description: "Restringem os eventos do webhook (marco 7). Cada filtro só vale para eventos que têm o campo: `instance.*` sempre passam."
      properties:
        chat: { type: string, enum: [direct, group] }
        chats: { type: array, maxItems: 500, items: { type: string }, description: "Só estes chats (número com DDI ou JID)." }
        exclude_chats: { type: array, items: { type: string }, description: Todos menos estes chats. }
        message_types:
          type: array
          items: { type: string, enum: [text, image, video, gif, ptv, audio, document, sticker, location, contact, poll, reaction, button_reply, unknown] }
          description: "Só eventos de mensagem destes tipos (received, sent, reaction, button_reply)."
        from_me: { type: boolean, description: "true: só mensagens nossas; false: exclui as nossas. Omitido: ambas. Não se aplica a message.status." }
    ChatPresenceEvent:
      type: object
      description: Dados de `chat.presence`.
      properties:
        chat: { type: string }
        sender: { type: string, description: Quem digita (o participante, em grupos). }
        is_group: { type: boolean }
        state: { type: string, enum: [composing, recording, paused, online, offline] }
        last_seen: { type: string, format: date-time, description: "Em `offline`, quando o contato mostra o visto por último." }
    CallEvent:
      type: object
      description: Dados de `call.received`, `call.accepted` e `call.ended`.
      properties:
        call_id: { type: string }
        chat: { type: string, description: Chat de quem ligou, ou o grupo em chamadas de grupo. }
        caller: { type: string }
        is_group: { type: boolean }
        media: { type: string, enum: [audio, video] }
        rejected: { type: boolean, description: "call.received: rejeitada por `calls.auto_reject`." }
        reason: { type: string, description: "call.ended: `caller_hangup`, `accepted_elsewhere`, `ended`, `rejected` (por esta instância), `rejected_by_peer`, `timeout` ou outro motivo do WhatsApp." }
        missed: { type: boolean, description: "call.ended: tocou sem ninguém atender." }
        timestamp: { type: string, format: date-time }
    GroupSettings:
      type: object
      description: Configurações de um grupo (marco 8).
      properties:
        announce: { type: boolean, description: Só admins enviam mensagens. }
        locked: { type: boolean, description: Só admins editam nome, descrição e foto. }
        approval_required: { type: boolean, description: Entrada com aprovação de um admin. }
        admins_add_members: { type: boolean, description: Só admins adicionam participantes. }
        ephemeral_seconds: { type: integer, enum: [0, 86400, 604800, 7776000], description: "Mensagens temporárias (0: desligadas)." }
    GroupParticipant:
      type: object
      properties:
        jid: { type: string, description: "JID de telefone quando conhecido; senão o LID." }
        lid: { type: string }
        phone: { type: string }
        admin: { type: boolean }
        super_admin: { type: boolean, description: Dono do grupo. }
    Group:
      type: object
      properties:
        jid: { type: string, example: "120363000000000001@g.us" }
        name: { type: string }
        description: { type: string }
        owner: { type: string }
        created_at: { type: string, format: date-time }
        community: { type: boolean, description: É uma comunidade. }
        parent: { type: string, description: Comunidade a que o grupo pertence. }
        announcement_group: { type: boolean, description: Grupo de avisos da comunidade. }
        participant_count: { type: integer }
        lid_addressing: { type: boolean, description: O grupo endereça os membros por LID. }
        suspended: { type: boolean }
        settings: { $ref: "#/components/schemas/GroupSettings" }
        participants: { type: array, items: { $ref: "#/components/schemas/GroupParticipant" }, description: "Só em `GET /groups/{jid}` e na criação." }
    SubGroup:
      type: object
      properties:
        jid: { type: string }
        name: { type: string }
        announcement_group: { type: boolean }
    CreateGroup:
      type: object
      required: [name]
      properties:
        name: { type: string, maxLength: 100 }
        participants: { type: array, maxItems: 10, items: { type: string }, description: Número com DDI ou JID. }
        description: { type: string, maxLength: 2048 }
        parent: { type: string, description: "Criar dentro desta comunidade (só em `POST /groups`)." }
        settings:
          type: object
          properties:
            announce: { type: boolean }
            locked: { type: boolean }
            approval_required: { type: boolean }
            admins_add_members: { type: boolean }
            ephemeral_seconds: { type: integer, enum: [0, 86400, 604800, 7776000] }
    UpdateGroup:
      type: object
      description: Campos ausentes ficam como estão. As configurações podem vir soltas ou em `settings`.
      properties:
        name: { type: string, maxLength: 100 }
        description: { type: string, maxLength: 2048, description: "\"\" remove." }
        announce: { type: boolean }
        locked: { type: boolean }
        approval_required: { type: boolean }
        admins_add_members: { type: boolean }
        ephemeral_seconds: { type: integer, enum: [0, 86400, 604800, 7776000] }
        settings: { $ref: "#/components/schemas/GroupSettings" }
    ParticipantResult:
      type: object
      properties:
        participant: { type: string, description: Como foi pedido (normalizado para JID). }
        jid: { type: string, description: Como o WhatsApp respondeu. }
        status:
          type: string
          description: "`ok`, `not_on_whatsapp`, `unknown_contact`, `privacy_invite_required`, `recently_left`, `already_participant`, `not_participant`, `group_full`, `error_<código>` ou `unknown`."
        code: { type: integer, description: Código do WhatsApp (melhor esforço). }
        invite_code: { type: string, description: "`privacy_invite_required`: código de convite por mensagem (não enviado)." }
        invite_expires_at: { type: string, format: date-time }
    CreateGroupResult:
      type: object
      properties:
        group: { $ref: "#/components/schemas/Group" }
        results: { type: array, items: { $ref: "#/components/schemas/ParticipantResult" } }
        warning: { type: string, description: "Um passo depois da criação falhou (descrição); o grupo existe." }
    GroupLimits:
      type: object
      description: "Cotas anti-ban de grupos por instância (marco 8). Tetos sobem só com `unsafe_pacing` (admin)."
      properties:
        max_adds_per_hour: { type: integer, default: 10, maximum: 30 }
        max_adds_per_day: { type: integer, default: 50, maximum: 200 }
        max_creates_per_day: { type: integer, default: 3, maximum: 10 }
        max_joins_per_day: { type: integer, default: 10, maximum: 30 }
    GroupUpdatedEvent:
      type: object
      description: Dados de `group.updated`.
      properties:
        chat: { type: string, description: JID do grupo. }
        kind: { type: string, enum: [participants_added, participants_removed, participants_promoted, participants_demoted, join_request, join_request_revoked, subject, description, photo, settings, invite_link_reset, joined, deleted, suspended, unsuspended, community_linked, community_unlinked] }
        actor: { type: string }
        participants: { type: array, items: { type: string } }
        reason: { type: string }
        name: { type: string }
        description: { type: string }
        removed: { type: boolean }
        settings:
          type: object
          properties:
            announce: { type: boolean }
            locked: { type: boolean }
            approval_required: { type: boolean }
            ephemeral_seconds: { type: integer }
            approval_mode_changed: { type: boolean, description: "A aprovação de entrada mudou e o valor novo não pôde ser lido." }
        group: { type: string }
        timestamp: { type: string, format: date-time }
    SettingsPatch:
      type: object
      description: Todos os campos são opcionais; ausentes mantêm o valor atual.
      properties:
        min_interval_ms: { type: integer }
        jitter_ms: { type: integer }
        max_per_minute: { type: integer }
        typing_simulation: { type: boolean }
        ramp:
          type: object
          properties:
            enabled: { type: boolean }
            start_per_day: { type: integer }
            end_per_day: { type: integer }
            days: { type: integer }
        send_window: { $ref: "#/components/schemas/SendWindow" }
        timezone: { type: string }
        unsafe_pacing: { type: boolean, description: Só admin. }
        proxy_url: { type: string, description: "Só admin. Vazio volta ao default_proxy_url." }
        calls:
          type: object
          properties:
            auto_reject: { type: boolean }
            reject_message: { type: string, maxLength: 1000 }
        presence:
          type: object
          properties:
            online: { type: boolean }
        groups: { $ref: "#/components/schemas/GroupLimits" }
        auto_read:
          type: object
          properties:
            enabled: { type: boolean }
            groups: { type: boolean }
    MediaInput:
      type: object
      description: Informe `url` ou `base64`.
      properties:
        url: { type: string, description: "http(s) público; o servidor baixa (timeout 30s, sem IPs privados)." }
        base64: { type: string }
        mime: { type: string }
        file_name: { type: string, description: Até 255 bytes. }
        ptt: { type: boolean, description: "Áudio como mensagem de voz." }
    SendMessage:
      type: object
      required: [to, type]
      properties:
        to: { type: string, description: "Número com DDI (10 a 15 dígitos) ou JID." }
        type: { type: string, enum: [text, image, video, audio, document, reaction] }
        text: { type: string, description: "Obrigatório em `text` (até 64KB)." }
        caption: { type: string, description: "Legenda de image, video e document." }
        media: { $ref: "#/components/schemas/MediaInput" }
        reaction:
          type: object
          properties:
            message_id: { type: string, description: "Id LiteZap ou wa_message_id de uma mensagem do chat." }
            emoji: { type: string, description: "Vazio remove a reação." }
        client_message_id: { type: string, maxLength: 128, description: Chave de idempotência. }
        expires_after: { type: string, description: "Duração entre 1m e 168h (padrão 24h). Não enviada até lá: message_expired.", example: 24h }
    MediaInfo:
      type: object
      properties:
        id: { type: string, description: Id da mídia recebida. }
        kind: { type: string }
        mime: { type: string }
        size: { type: integer }
        file_name: { type: string }
        seconds: { type: integer }
        ptt: { type: boolean }
        url: { type: string, description: Caminho da API para baixar (mídia recebida). }
    Message:
      type: object
      required: [id, chat, from_me, type, status, timestamp]
      properties:
        id: { type: string }
        client_message_id: { type: string }
        wa_message_id: { type: string }
        chat: { type: string }
        sender: { type: string }
        from_me: { type: boolean }
        source: { type: string, enum: [contact, api, device, history] }
        type: { type: string }
        text: { type: string }
        media: { $ref: "#/components/schemas/MediaInfo" }
        reaction_to: { type: string }
        status:
          type: string
          enum: [queued, in_flight, sent, delivered, read, played, failed, received, pending]
        attempts: { type: integer }
        error: { type: string, description: "Último erro (`codigo: detalhe`) de item não enviado." }
        timestamp: { type: string, format: date-time }
    Chat:
      type: object
      properties:
        jid: { type: string }
        name: { type: string }
        is_group: { type: boolean }
        last_message_at: { type: string, format: date-time }
        unread_count: { type: integer }
        archived: { type: boolean }
        pinned: { type: boolean }
        muted: { type: boolean }
        muted_until: { type: string, format: date-time, description: "Fim do silêncio; ausente com `muted: true` = até reativar." }
        unread: { type: boolean, description: "Marcado como não lido à mão (`unread_count` conta as recebidas)." }
        deleted_at: { type: string, format: date-time, description: Quando foi apagado (o chat volta à lista com uma mensagem nova). }
    ChatPatch:
      type: object
      description: Campos ausentes não mudam; pelo menos um é obrigatório.
      properties:
        archived: { type: boolean }
        pinned: { type: boolean }
        muted: { type: boolean }
        mute_seconds: { type: integer, minimum: 0, maximum: 31622400, description: "Com `muted: true`: duração; 0 ou ausente = até reativar." }
        unread: { type: boolean, description: "true marca como não lido; false lê (e manda os recibos)." }
    Picture:
      type: object
      properties:
        jid: { type: string }
        id: { type: string, description: Muda quando a foto muda. }
        url: { type: string, description: CDN do WhatsApp; expira. }
        type: { type: string, enum: [image, preview] }
    ContactProfile:
      type: object
      properties:
        jid: { type: string }
        lid: { type: string }
        about: { type: string }
        picture_id: { type: string }
        verified_name: { type: string, description: Contas business. }
        business:
          type: object
          properties:
            address: { type: string }
            email: { type: string }
            categories: { type: array, items: { type: string } }
    Blocklist:
      type: object
      properties:
        blocked: { type: array, items: { type: string } }
    Profile:
      type: object
      properties:
        jid: { type: string }
        lid: { type: string }
        name: { type: string, description: Nome de perfil (push name). }
        about: { type: string }
        business_name: { type: string }
        platform: { type: string, description: Plataforma do celular pareado. }
        picture: { $ref: "#/components/schemas/Picture" }
    ProfilePatch:
      type: object
      properties:
        name: { type: string, minLength: 1, maxLength: 25 }
        about: { type: string, maxLength: 139 }
    PictureInput:
      type: object
      description: Exatamente um dos campos.
      properties:
        url: { type: string, format: uri }
        base64: { type: string }
    Privacy:
      type: object
      properties:
        last_seen: { type: string, enum: [all, contacts, contact_blacklist, none] }
        online: { type: string, enum: [all, match_last_seen] }
        profile_photo: { type: string, enum: [all, contacts, contact_blacklist, none] }
        about: { type: string, enum: [all, contacts, contact_blacklist, none] }
        groups_add: { type: string, enum: [all, contacts, contact_blacklist, none] }
        read_receipts: { type: string, enum: [all, none] }
        calls: { type: string, enum: [all, known] }
        messages: { type: string, enum: [all, contacts] }
    PrivacyPatch:
      allOf:
        - $ref: "#/components/schemas/Privacy"
        - type: object
          properties:
            default_disappearing_seconds: { type: integer, enum: [0, 86400, 604800, 7776000] }
    ChatUpdatedEvent:
      type: object
      description: Dados de `chat.updated`.
      properties:
        chat: { type: string }
        is_group: { type: boolean }
        changed: { type: array, items: { type: string, enum: [archived, pinned, muted, unread, deleted, cleared, starred] } }
        archived: { type: boolean }
        pinned: { type: boolean }
        muted: { type: boolean }
        muted_until: { type: string, format: date-time }
        unread: { type: boolean }
        deleted: { type: boolean }
        cleared: { type: boolean }
        message_id: { type: string }
        wa_message_id: { type: string }
        starred: { type: boolean }
        source: { type: string, enum: [api, device] }
        timestamp: { type: string, format: date-time }
    ChatPage:
      type: object
      properties:
        chats: { type: array, items: { $ref: "#/components/schemas/Chat" } }
        next_cursor: { type: string, description: Ausente na última página. }
    MessagePage:
      type: object
      properties:
        messages: { type: array, items: { $ref: "#/components/schemas/Message" } }
        next_cursor: { type: string, description: Ausente na última página. }
    Contact:
      type: object
      properties:
        jid: { type: string }
        name: { type: string }
        push_name: { type: string }
        business_name: { type: string }
    NumberResult:
      type: object
      properties:
        phone: { type: string }
        exists: { type: boolean }
        jid: { type: string }
    SignedURL:
      type: object
      properties:
        url: { type: string }
        expires_at: { type: string, format: date-time }
    WebhookInput:
      type: object
      properties:
        url: { type: string, description: "URL absoluta http(s), até 2048 caracteres. Obrigatório na criação." }
        events:
          type: array
          maxItems: 32
          items: { type: string }
          description: "Filtros: tipos exatos, famílias (`message.*`) ou `*`. Obrigatório na criação."
        secret: { type: string, description: "16 a 256 caracteres. Omitido na criação: gerado. Vazio no PATCH: gera outro." }
        instance_id: { type: string, description: "Só na criação. Admin: instância do webhook (omitido = global). Token de instância: só a própria." }
        enabled: { type: boolean, default: true }
        version: { type: integer, enum: [1], description: Versão do envelope. Só 1 por enquanto. }
        filters: { $ref: "#/components/schemas/WebhookFilters" }
    Webhook:
      type: object
      properties:
        id: { type: string }
        instance_id: { type: string, description: Ausente em webhook global. }
        url: { type: string }
        events: { type: array, items: { type: string } }
        version: { type: integer }
        enabled: { type: boolean }
        filters: { $ref: "#/components/schemas/WebhookFilters" }
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }
        secret: { type: string, description: Só na criação e quando trocado por PATCH. }
    Delivery:
      type: object
      properties:
        id: { type: string }
        webhook_id: { type: string }
        event_id: { type: string }
        event_type: { type: string }
        instance_id: { type: string }
        status: { type: string, enum: [pending, delivered, failed] }
        attempts: { type: integer }
        next_attempt_at: { type: string, format: date-time }
        expires_at: { type: string, format: date-time, description: Prazo para entregar antes de ir para a dead-letter. }
        last_status_code: { type: integer }
        last_error: { type: string, description: "Ex.: `http 503`, `timeout: ...`, `blocked: ...`." }
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }
        delivered_at: { type: string, format: date-time }
        event: { $ref: "#/components/schemas/Envelope" }
    DeliveryPage:
      type: object
      properties:
        deliveries: { type: array, items: { $ref: "#/components/schemas/Delivery" } }
        next_cursor: { type: string }
    Envelope:
      type: object
      required: [id, type, version, instance_id, timestamp, data]
      properties:
        id: { type: string, description: "Id do evento (`evt_...`). Use para deduplicar." }
        type: { type: string }
        version: { type: integer, enum: [1] }
        instance_id: { type: string }
        timestamp: { type: string, format: date-time }
        data: { type: object, description: Varia por tipo de evento. }
    EventMessage:
      type: object
      description: Mensagem em `message.received`, `message.sent`, `message.reaction` e `message.button_reply`.
      properties:
        id: { type: string, description: "Id LiteZap (em envios pela API, o id do item da fila)." }
        wa_message_id: { type: string }
        chat: { type: string }
        sender: { type: string }
        from_me: { type: boolean }
        is_group: { type: boolean }
        push_name: { type: string, description: Nome de perfil de quem enviou. }
        source: { type: string, enum: [contact, api, device] }
        type: { type: string }
        text: { type: string, description: "Texto, legenda ou emoji da reação." }
        media: { $ref: "#/components/schemas/MediaInfo" }
        reaction_to: { type: string }
        button_reply:
          type: object
          description: "Só em `message.button_reply`."
          properties:
            kind: { type: string, enum: [button, list, template, interactive] }
            id: { type: string }
            title: { type: string }
            description: { type: string }
        timestamp: { type: string, format: date-time }

    StatusInput:
      type: object
      required: [type]
      properties:
        type: { type: string, enum: [text, image, video] }
        text: { type: string, maxLength: 700, description: "text: o texto do status." }
        caption: { type: string, description: "image/video: legenda." }
        media: { $ref: "#/components/schemas/MediaInput" }
        background_color: { type: string, example: "#1E6E4F", description: "text: cor de fundo #RRGGBB." }
        text_color: { type: string, example: "#FFFFFF" }
        font: { type: integer, enum: [0, 1, 2, 6, 7, 8, 9, 10] }
        client_message_id: { type: string }
        expires_after: { type: string, example: 2h }
    StatusPrivacy:
      type: object
      properties:
        type: { type: string, enum: [contacts, blacklist, whitelist], description: "contacts: todos; blacklist: todos menos `list`; whitelist: só `list`." }
        list: { type: array, items: { type: string } }
    Channel:
      type: object
      properties:
        jid: { type: string, example: 120363000000000001@newsletter }
        name: { type: string }
        description: { type: string }
        invite_code: { type: string }
        invite_link: { type: string }
        subscribers: { type: integer }
        verified: { type: boolean }
        state: { type: string, enum: [active, suspended, geosuspended] }
        created_at: { type: string, format: date-time }
        role: { type: string, enum: [owner, admin, subscriber, guest], description: "Papel do número (vazio quando lido por convite)." }
        muted: { type: boolean }
        picture_url: { type: string }
    CreateChannel:
      type: object
      required: [name]
      properties:
        name: { type: string, maxLength: 100 }
        description: { type: string, maxLength: 2048 }
        picture: { $ref: "#/components/schemas/PictureInput" }
    ChannelPost:
      type: object
      properties:
        server_id: { type: integer, description: "Número da publicação no canal (reações usam)." }
        wa_message_id: { type: string }
        type: { type: string }
        text: { type: string }
        media: { $ref: "#/components/schemas/MediaInfo" }
        views: { type: integer }
        reactions: { type: object, additionalProperties: { type: integer } }
        timestamp: { type: string, format: date-time }
    ChannelPostInput:
      type: object
      required: [type]
      properties:
        type: { type: string, enum: [text, image, video, audio, document] }
        text: { type: string }
        caption: { type: string }
        media: { $ref: "#/components/schemas/MediaInput" }
        client_message_id: { type: string }
        expires_after: { type: string }
    Label:
      type: object
      properties:
        id: { type: string }
        name: { type: string }
        color: { type: integer, minimum: 0, maximum: 19, description: "Índice na paleta do WhatsApp." }
        chats: { type: array, items: { type: string } }
    LabelInput:
      type: object
      properties:
        name: { type: string, maxLength: 100 }
        color: { type: integer, minimum: 0, maximum: 19 }
    BusinessProfile:
      type: object
      properties:
        jid: { type: string }
        business_name: { type: string }
        address: { type: string }
        email: { type: string }
        categories: { type: array, items: { type: string } }
        timezone: { type: string }
        hours:
          type: array
          items:
            type: object
            properties:
              day: { type: string }
              mode: { type: string }
              open: { type: string }
              close: { type: string }
        options: { type: object, additionalProperties: { type: string } }
    ChannelMessageEvent:
      type: object
      description: Dados de `channel.message`.
      properties:
        channel: { type: string }
        server_id: { type: integer }
        wa_message_id: { type: string }
        type: { type: string }
        text: { type: string }
        media: { $ref: "#/components/schemas/MediaInfo" }
        views: { type: integer }
        reactions: { type: object, additionalProperties: { type: integer } }
        timestamp: { type: string, format: date-time }
    LabelUpdatedEvent:
      type: object
      description: Dados de `label.updated`.
      properties:
        kind: { type: string, enum: [edit, chat, message] }
        label_id: { type: string }
        name: { type: string }
        color: { type: integer }
        deleted: { type: boolean }
        chat: { type: string }
        wa_message_id: { type: string }
        labeled: { type: boolean }
        source: { type: string, enum: [api, device] }
        timestamp: { type: string, format: date-time }
    AgentHook:
      type: object
      properties:
        configured: { type: boolean }
        target: { type: string, enum: [url, builtin], description: "url: POST assinado para `url`; builtin: Atendente LiteZap embutido." }
        url: { type: string }
        enabled: { type: boolean }
        timeout: { type: string, example: 30s }
        context_messages: { type: integer }
        groups_enabled: { type: boolean }
        human_pause: { type: string, example: 30m0s }
        pause_on_api_send: { type: boolean }
        ignore: { type: array, items: { type: string } }
        max_replies_per_window: { type: integer }
        reply_window: { type: string }
        secret_set: { type: boolean }
        secret: { type: string, description: Só quando acabou de ser gerado. }
    AgentHookPatch:
      type: object
      additionalProperties: false
      properties:
        target: { type: string, enum: [url, builtin] }
        url: { type: string }
        secret: { type: string }
        enabled: { type: boolean }
        timeout: { type: string }
        context_messages: { type: integer, minimum: 0, maximum: 100 }
        groups_enabled: { type: boolean }
        human_pause: { type: string }
        pause_on_api_send: { type: boolean }
        ignore: { type: array, items: { type: string } }
        max_replies_per_window: { type: integer }
        reply_window: { type: string }
    AttendantAudio:
      type: object
      properties:
        base_url: { type: string, description: "Servidor OpenAI-compatível; vazio com provider openai = o mesmo servidor." }
        transcription_model: { type: string, description: "Ex.: whisper-1; vazio desliga a transcrição." }
        speech_model: { type: string, description: "Ex.: gpt-4o-mini-tts; vazio desliga a voz." }
        voice: { type: string }
    AttendantSettings:
      type: object
      properties:
        configured: { type: boolean }
        provider: { type: string, enum: [anthropic, openai] }
        model: { type: string, example: claude-sonnet-5-5 }
        base_url: { type: string }
        max_tokens: { type: integer }
        daily_call_cap: { type: integer }
        chat_daily_call_cap: { type: integer }
        simulate_daily_cap: { type: integer }
        active_agent_id: { type: string }
        vision: { type: boolean }
        audio: { $ref: "#/components/schemas/AttendantAudio" }
        price_input_per_mtok: { type: number }
        price_output_per_mtok: { type: number }
        api_key_set: { type: boolean }
        audio_api_key_set: { type: boolean }
        ready: { type: boolean }
        not_ready: { type: string }
        transcription: { type: boolean }
        speech: { type: boolean }
        today: { type: object, properties: { day: { type: string }, calls: { type: integer }, input_tokens: { type: integer }, output_tokens: { type: integer }, cost_usd: { type: number } } }
    AttendantSettingsPatch:
      type: object
      additionalProperties: false
      properties:
        provider: { type: string, enum: [anthropic, openai] }
        model: { type: string }
        base_url: { type: string }
        max_tokens: { type: integer, minimum: 64, maximum: 8192 }
        daily_call_cap: { type: integer, minimum: 0 }
        chat_daily_call_cap: { type: integer, minimum: 0, description: "Chamadas por conversa por dia (padrão 40)." }
        simulate_daily_cap: { type: integer, minimum: 0, description: "Chamadas do simulador por dia (padrão 50)." }
        vision: { type: boolean }
        audio: { $ref: "#/components/schemas/AttendantAudio" }
        price_input_per_mtok: { type: number }
        price_output_per_mtok: { type: number }
        api_key: { type: string, writeOnly: true }
        audio_api_key: { type: string, writeOnly: true }
    AttendantProfile:
      type: object
      required: [name]
      properties:
        name: { type: string, maxLength: 80 }
        mode: { type: string, enum: [vendedor, pedido, sdr, suporte, custom] }
        objective: { type: string }
        steps: { type: array, items: { type: string } }
        tone: { type: string }
        handoff_criteria: { type: array, items: { type: string } }
        actions: { type: array, items: { type: string, enum: [handoff, lead, appointment, order_draft] } }
        business: { type: string, description: Contexto do negócio (texto livre, confiável). }
        knowledge:
          type: array
          maxItems: 300
          items:
            type: object
            properties:
              kind: { type: string, enum: [faq, product, hours, policy, info] }
              title: { type: string }
              content: { type: string }
              price: { type: number }
        language: { type: string, default: pt-BR }
        currency: { type: string, default: BRL }
        voice_reply: { type: string, enum: ["off", when_audio, always], default: "off" }
        call_followup: { type: boolean }
        call_hint: { type: string }
        max_reply_chars: { type: integer, minimum: 50, maximum: 4000, default: 1000 }
        hide_contact_name: { type: boolean }
    AttendantAgent:
      allOf:
        - { $ref: "#/components/schemas/AttendantProfile" }
        - type: object
          properties:
            id: { type: string }
            active: { type: boolean }
            created_at: { type: string, format: date-time }
            updated_at: { type: string, format: date-time }
  responses:
    InstanceOK:
      description: A instância logo após a ação (o estado pode ainda ser o anterior).
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Instance" }
    ValidationFailed:
      description: "`validation_failed`: corpo ou campo inválido."
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
          example: { error: { code: validation_failed, message: "phone must have 10 to 15 digits including country code", retryable: false } }
    SendBadRequest:
      description: "`validation_failed` ou `invalid_number` (o número não tem WhatsApp)."
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
          example: { error: { code: invalid_number, message: "number is not on WhatsApp", retryable: false } }
    Unauthorized:
      description: "`unauthorized`: token ausente ou inválido."
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
          example: { error: { code: unauthorized, message: "missing or invalid token", retryable: false } }
    Forbidden:
      description: "`forbidden`: token sem acesso ao recurso."
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
          example: { error: { code: forbidden, message: "token not allowed for this resource", retryable: false } }
    InstanceNotFound:
      description: "`instance_not_found`."
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
          example: { error: { code: instance_not_found, message: "instance not found", retryable: false } }
    WebhookNotFound:
      description: "`not_found`: webhook inexistente ou de outra instância."
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
          example: { error: { code: not_found, message: "webhook not found", retryable: false } }
    RetryConflict:
      description: "`conflict`: só entregas `failed` podem ser reenviadas."
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
          example: { error: { code: conflict, message: "only failed deliveries can be retried (status: pending)", retryable: false } }
    NotFound:
      description: "`not_found` (mensagem, chat, contato ou mídia) ou `instance_not_found`."
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
          example: { error: { code: not_found, message: "message not found", retryable: false } }
    ChatConflict:
      description: "`app_state_not_synced` (o celular ainda não mandou as chaves do app state; tente de novo), `pin_limit` (já há 3 fixados), `instance_not_connected` ou `instance_banned`."
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
          example: { error: { code: app_state_not_synced, message: "chat state keys not synced from the phone yet; keep the phone online and try again shortly", retryable: true } }
    LookupRateLimited:
      description: "`rate_limited`: cota de consultas de contato esgotada (30/min, 1000/dia por instância)."
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
          example: { error: { code: rate_limited, message: "too many contact lookups; at most 30 per minute and 1000 per day per instance", retryable: true } }
    InstanceConflict:
      description: "`instance_not_connected` (worker parado ou número deslogado) ou `instance_banned`."
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
          example: { error: { code: instance_not_connected, message: "instance worker is not running", retryable: true } }
    NotPairing:
      description: "`instance_not_pairing`: a instância não está aguardando pareamento."
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
          example: { error: { code: instance_not_pairing, message: "instance is not waiting for a QR scan (state: connected)", retryable: false } }
    SendConflict:
      description: "`conflict` (client_message_id reusado com outro conteúdo), `instance_not_connected` (deslogada) ou `instance_banned`."
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
          example: { error: { code: conflict, message: "client_message_id was already used for a different message", retryable: false } }
    SignedDisabled:
      description: "`conflict`: URLs assinadas desativadas (fora de loopback sem public_url)."
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
          example: { error: { code: conflict, message: "signed URLs need public_url when the API is not bound to loopback", retryable: false } }
    MessageExpired:
      description: "`message_expired`: não enviada antes de expires_after (com ?wait=true)."
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
          example: { error: { code: message_expired, message: "message not sent before expires_after", retryable: false } }
    MediaUnavailable:
      description: "`media_unavailable`: a mídia expirou no WhatsApp."
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
          example: { error: { code: media_unavailable, message: "media is no longer available on WhatsApp", retryable: false } }
    MediaTooLarge:
      description: "`media_too_large`: acima do limite do tipo ou corpo acima de 140MB."
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
          example: { error: { code: media_too_large, message: "image exceeds the size limit", retryable: false } }
    MediaFetchFailed:
      description: "`media_fetch_failed`: media.url não pôde ser baixada."
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
          example: { error: { code: media_fetch_failed, message: "media.url could not be downloaded", retryable: true } }
    RateLimited:
      description: "`rate_limited`: cota de consultas de check-numbers esgotada (10/min, 200/dia)."
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
          example: { error: { code: rate_limited, message: "too many number lookups; try again in a minute", retryable: true } }
    BusyOrShuttingDown:
      description: "`rate_limited` (muitos envios grandes simultâneos e o cliente desistiu de esperar vaga) ou `shutting_down`."
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
          example: { error: { code: rate_limited, message: "too many large sends in progress", retryable: true } }
    ShuttingDown:
      description: "`shutting_down`: servidor encerrando."
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
          example: { error: { code: shutting_down, message: "server is shutting down", retryable: true } }
    WorkerError:
      description: "`worker_error`: o worker recusou ou falhou a operação."
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
          example: { error: { code: worker_error, message: "worker request failed", retryable: true } }
    GroupNotFound:
      description: "`group_not_found`: grupo inexistente ou a instância não participa dele (ou `instance_not_found`)."
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
          example: { error: { code: group_not_found, message: "group not found (or the instance is not in it)", retryable: false } }
    GroupForbidden:
      description: "`group_forbidden`: o WhatsApp recusou (a instância não é admin ou não participa); ou `forbidden` (token)."
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
          example: { error: { code: group_forbidden, message: "not allowed: the account must be a member (or an admin) of the group", retryable: false } }
    GroupRateLimited:
      description: "`rate_limited`: cota de grupos (`settings.groups`), 10s entre lotes de adição, cota de consultas ao checar participantes, ou limite do próprio WhatsApp."
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
          example: { error: { code: rate_limited, message: "participant add limit reached: at most 10 per hour per instance (0 left; settings.groups.max_adds_per_hour)", retryable: true } }
    InviteRevoked:
      description: "`invite_revoked`: o link de convite foi revogado."
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
          example: { error: { code: invite_revoked, message: "the invite link was revoked", retryable: false } }
    WorkerTimeout:
      description: "`worker_timeout`: o worker não respondeu a tempo."
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
          example: { error: { code: worker_timeout, message: "instance worker did not answer in time", retryable: true } }

# Eventos entregues por webhook e SSE (catálogo v1). Fonte no motor: internal/events.
    SocialRateLimited:
      description: "`rate_limited`: limite da instância (10 status/dia; 20 publicações em canal/dia; seguir canais 3/min, 10/h e 30/dia; silenciar 10/min; criar canal 2/dia; reagir 30/min; etiquetas 30/min)."
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
          example: { error: { code: rate_limited, message: "daily status (10 per day) limit reached for this instance; try again later", retryable: true } }
    BusinessRequired:
      description: "`business_account_required`: só contas WhatsApp Business; ou `app_state_not_synced` logo após parear."
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
          example: { error: { code: business_account_required, message: "this needs a WhatsApp Business account", retryable: false } }
webhooks:
  instance.status:
    post:
      summary: Estado da instância mudou
      x-slug: instance-status
      x-litezap-milestone: M3
      x-zapi: Ao conectar / Ao desconectar
      description: |
        Emitido em toda transição de estado da instância (`starting`, `qr_pending`, `connecting`,
        `connected`, `reconnecting`, `logged_out`, `banned`, `failed`, `stopped`). `reason` explica alguns
        estados, como `proxy_unreachable` em `reconnecting` ou `temp_banned:<duração>` em `failed`.
      x-related: [instance.qr, instance.pair_code]
      x-fields:
        - [state, string, "Novo estado. Veja Instâncias e estados."]
        - [reason, string, "Motivo, quando houver (opcional)."]
      requestBody:
        content:
          application/json:
            schema: { $ref: "#/components/schemas/Envelope" }
            examples:
              conectada:
                summary: Conectou
                value: { id: evt_0a7d3c9e1f5b2486, type: instance.status, version: 1, instance_id: inst_7f3a9c2e5b1d4086, timestamp: "2026-10-17T13:40:02.511Z", data: { state: connected } }
              proxy:
                summary: Proxy fora do ar
                value: { id: evt_6b2e8d0a4c1f7395, type: instance.status, version: 1, instance_id: inst_7f3a9c2e5b1d4086, timestamp: "2026-10-17T15:12:44.020Z", data: { state: reconnecting, reason: proxy_unreachable } }
      responses:
        "2XX": { description: Qualquer 2xx confirma a entrega. }
  instance.qr:
    post:
      summary: Novo QR de pareamento
      x-slug: instance-qr
      x-litezap-milestone: M3
      x-zapi: (sem equivalente; QR por polling)
      description: |
        Emitido a cada novo QR enquanto a instância está em `qr_pending`. Mostre `code` como QR (qualquer
        biblioteca de QR serve) em vez de fazer polling em `GET /qr`. `timeout_ms` é a validade aproximada
        deste código.
      x-related: [instance.status, instance.pair_code]
      x-fields:
        - [code, string, "Conteúdo do QR."]
        - [timeout_ms, integer, "Validade do código em milissegundos (opcional)."]
      requestBody:
        content:
          application/json:
            schema: { $ref: "#/components/schemas/Envelope" }
            example: { id: evt_1c8f3a6e9d2b4057, type: instance.qr, version: 1, instance_id: inst_7f3a9c2e5b1d4086, timestamp: "2026-10-17T13:39:10.004Z", data: { code: "2@kQ3x...,Zm9v...,YmFy...,MQ==", timeout_ms: 20000 } }
      responses:
        "2XX": { description: Qualquer 2xx confirma a entrega. }
  instance.pair_code:
    post:
      summary: Código de pareamento gerado
      x-slug: instance-pair-code
      x-litezap-milestone: M3
      x-zapi: (sem equivalente)
      description: Emitido quando `POST /pair-phone` gera um código para digitar no celular.
      x-related: [instance.status, instance.qr]
      x-fields:
        - [code, string, "Código para digitar em Aparelhos conectados > Conectar com número de telefone."]
      requestBody:
        content:
          application/json:
            schema: { $ref: "#/components/schemas/Envelope" }
            example: { id: evt_9e4b1d7a3f0c6285, type: instance.pair_code, version: 1, instance_id: inst_7f3a9c2e5b1d4086, timestamp: "2026-10-17T13:39:30.718Z", data: { code: "ABCD-EFGH" } }
      responses:
        "2XX": { description: Qualquer 2xx confirma a entrega. }
  message.received:
    post:
      summary: Mensagem recebida
      x-slug: message-received
      x-litezap-milestone: M3
      x-zapi: Ao receber
      description: |
        Emitido para cada mensagem nova de outra pessoa (`source: "contact"`), em chat direto ou grupo.
        Mídia vem descrita em `media`, com `url` para baixar pela API (autenticada). Mensagens do histórico
        sincronizado no pareamento não geram este evento (só um `history.synced`), e uma mensagem que chega
        duas vezes (reconexão do worker) gera um único evento. Reações chegam como `message.reaction`.
      x-related: [message.reaction, message.sent, history.synced]
      x-fields:
        - [id, string, "Id LiteZap da mensagem no log."]
        - [wa_message_id, string, "Id da mensagem no WhatsApp."]
        - [chat, string, "JID do chat (pessoa ou grupo)."]
        - [sender, string, "JID de quem enviou (em grupos, o participante)."]
        - [from_me, boolean, "Sempre false aqui."]
        - [is_group, boolean, "true em grupos."]
        - [push_name, string, "Nome de perfil de quem enviou."]
        - [source, string, "contact."]
        - [type, string, "text, image, video, audio, document, sticker, location, contact..."]
        - [text, string, "Texto ou legenda."]
        - [media, object, "id, kind, mime, size, file_name, seconds, ptt, url (opcional)."]
        - [timestamp, string, "Hora da mensagem (RFC 3339)."]
      requestBody:
        content:
          application/json:
            schema: { $ref: "#/components/schemas/Envelope" }
            examples:
              texto:
                summary: Texto
                value: { id: evt_5f2c9a1b7d3e4f60, type: message.received, version: 1, instance_id: inst_7f3a9c2e5b1d4086, timestamp: "2026-10-17T13:45:00.123Z", data: { id: msg_e29d4b8a0f6c1357, wa_message_id: 3A9F2C7D1E0B4A5C6D7E, chat: 5511999999999@s.whatsapp.net, sender: 5511999999999@s.whatsapp.net, from_me: false, push_name: Maria, source: contact, type: text, text: "Oi, meu pedido já saiu?", timestamp: "2026-10-17T13:45:00Z" } }
              imagem:
                summary: Imagem com legenda
                value: { id: evt_2d6a9f1c4e8b3075, type: message.received, version: 1, instance_id: inst_7f3a9c2e5b1d4086, timestamp: "2026-10-17T13:46:12.877Z", data: { id: msg_4f0b7c2e9a1d6358, wa_message_id: 3A1B5D9F2C7E4A0B8C6D, chat: 5511999999999@s.whatsapp.net, sender: 5511999999999@s.whatsapp.net, from_me: false, push_name: Maria, source: contact, type: image, text: "Foto do produto", media: { id: med_5b8e1f3c9d2a7046, kind: image, mime: image/jpeg, size: 182345, url: /v1/instances/inst_7f3a9c2e5b1d4086/media/med_5b8e1f3c9d2a7046 }, timestamp: "2026-10-17T13:46:12Z" } }
              voz:
                summary: Mensagem de voz
                value: { id: evt_7a3d0f6b2c9e1484, type: message.received, version: 1, instance_id: inst_7f3a9c2e5b1d4086, timestamp: "2026-10-17T13:47:30.402Z", data: { id: msg_9c2e5a8d1f4b7063, wa_message_id: 3A7E2B9D4F1C6A0E5B8D, chat: 5511999999999@s.whatsapp.net, sender: 5511999999999@s.whatsapp.net, from_me: false, push_name: Maria, source: contact, type: audio, media: { id: med_1d6f3b9e4a2c8057, kind: audio, mime: "audio/ogg; codecs=opus", size: 23110, seconds: 7, ptt: true, url: /v1/instances/inst_7f3a9c2e5b1d4086/media/med_1d6f3b9e4a2c8057 }, timestamp: "2026-10-17T13:47:30Z" } }
              grupo:
                summary: Texto em grupo
                value: { id: evt_3e9b6d2a0f7c5148, type: message.received, version: 1, instance_id: inst_7f3a9c2e5b1d4086, timestamp: "2026-10-17T13:50:05.611Z", data: { id: msg_6a1d8f3c0e5b9274, wa_message_id: 3A0C4E8B2D6F1A9C7E3B, chat: 120363000000000001@g.us, sender: 5511988887777@s.whatsapp.net, from_me: false, is_group: true, push_name: João, source: contact, type: text, text: "Reunião às 15h", timestamp: "2026-10-17T13:50:05Z" } }
      responses:
        "2XX": { description: Qualquer 2xx confirma a entrega. }
  message.sent:
    post:
      summary: Mensagem enviada
      x-slug: message-sent
      x-litezap-milestone: M3
      x-zapi: Ao enviar / Notificar enviadas por mim
      description: |
        Emitido quando um envio pela API é confirmado pelo WhatsApp (`source: "api"`, `id` = id do item da
        fila) ou quando alguém envia uma mensagem pelo celular do número conectado (`source: "device"`).
        Útil para detectar que um humano respondeu pelo celular.
      x-related: [message.status, message.received]
      x-fields:
        - [id, string, "Id LiteZap (em envios pela API, o id devolvido por POST /messages)."]
        - [wa_message_id, string, "Id no WhatsApp."]
        - [chat, string, "JID do chat."]
        - [from_me, boolean, "Sempre true."]
        - [source, string, "api ou device."]
        - [type, string, "Tipo da mensagem."]
        - [text, string, "Texto ou legenda."]
        - [timestamp, string, "Hora do envio."]
      requestBody:
        content:
          application/json:
            schema: { $ref: "#/components/schemas/Envelope" }
            examples:
              api:
                summary: Enviada pela API
                value: { id: evt_8c4f1a7d3b0e6259, type: message.sent, version: 1, instance_id: inst_7f3a9c2e5b1d4086, timestamp: "2026-10-17T13:50:03.090Z", data: { id: msg_a41c9e07d2b35f68, wa_message_id: 3EB0A1B2C3D4E5F6A7B8C9, chat: 5511999999999@s.whatsapp.net, from_me: true, source: api, type: text, text: "Seu pedido saiu para entrega!", timestamp: "2026-10-17T13:50:03Z" } }
              celular:
                summary: Enviada pelo celular
                value: { id: evt_4b0e7c3a9f2d6158, type: message.sent, version: 1, instance_id: inst_7f3a9c2e5b1d4086, timestamp: "2026-10-17T13:55:41.330Z", data: { id: msg_2e7a4c9f1b6d3085, wa_message_id: 3AF1C8E2B5D9A4F7C0E1, chat: 5511999999999@s.whatsapp.net, from_me: true, source: device, type: text, text: "Já está a caminho!", timestamp: "2026-10-17T13:55:41Z" } }
      responses:
        "2XX": { description: Qualquer 2xx confirma a entrega. }
  message.status:
    post:
      summary: Status de mensagem enviada
      x-slug: message-status
      x-litezap-milestone: M3
      x-zapi: Status da mensagem
      description: |
        Acompanha as mensagens enviadas pela API: `sent`, `delivered`, `read` ou `failed` (com `error` no
        formato `codigo: detalhe`). Se um recibo chegar antes da confirmação do envio, `delivered` (ou
        `read`) pode chegar antes de `message.sent`, e o `sent` é omitido. `played` (M7) sai quando o
        contato reproduz um áudio de voz, vídeo ou mídia de visualização única; é o status mais alto
        (um `read` atrasado não o rebaixa). Mensagem apagada sai em `message.revoked`.
      x-related: [message.sent]
      x-fields:
        - [id, string, "Id devolvido por POST /messages."]
        - [wa_message_id, string, "Id no WhatsApp (opcional)."]
        - [chat, string, "JID do chat."]
        - [status, string, "sent, delivered, read, played ou failed."]
        - [error, string, "Motivo, em failed (opcional)."]
      requestBody:
        content:
          application/json:
            schema: { $ref: "#/components/schemas/Envelope" }
            examples:
              lida:
                summary: Lida
                value: { id: evt_0f5c2a8e6d1b9374, type: message.status, version: 1, instance_id: inst_7f3a9c2e5b1d4086, timestamp: "2026-10-17T13:51:20.144Z", data: { id: msg_a41c9e07d2b35f68, wa_message_id: 3EB0A1B2C3D4E5F6A7B8C9, chat: 5511999999999@s.whatsapp.net, status: read } }
              falhou:
                summary: Falhou
                value: { id: evt_6d1a9e4c2f7b0583, type: message.status, version: 1, instance_id: inst_7f3a9c2e5b1d4086, timestamp: "2026-10-17T13:52:00.010Z", data: { id: msg_0c5e8b2f9a7d4613, chat: 5511888888888@s.whatsapp.net, status: failed, error: "invalid_number: number is not on WhatsApp" } }
      responses:
        "2XX": { description: Qualquer 2xx confirma a entrega. }
  message.reaction:
    post:
      summary: Reação recebida
      x-slug: message-reaction
      x-litezap-milestone: M3
      x-zapi: Ao receber (reação)
      description: Emitido quando alguém reage a uma mensagem. `text` é o emoji (vazio = reação removida) e `reaction_to` o id WhatsApp da mensagem reagida.
      x-related: [message.received]
      x-fields:
        - [type, string, "reaction."]
        - [text, string, "Emoji; vazio quando a reação foi removida."]
        - [reaction_to, string, "wa_message_id da mensagem reagida."]
        - [chat, string, "JID do chat."]
        - [sender, string, "Quem reagiu."]
      requestBody:
        content:
          application/json:
            schema: { $ref: "#/components/schemas/Envelope" }
            example: { id: evt_2a7e4c1f9b6d3058, type: message.reaction, version: 1, instance_id: inst_7f3a9c2e5b1d4086, timestamp: "2026-10-17T13:53:08.555Z", data: { id: msg_7d3b0e6a2c9f4185, wa_message_id: 3A5D8F1B4E7C0A3D6F9B, chat: 5511999999999@s.whatsapp.net, sender: 5511999999999@s.whatsapp.net, from_me: false, push_name: Maria, source: contact, type: reaction, text: "👍", reaction_to: 3EB0A1B2C3D4E5F6A7B8C9, timestamp: "2026-10-17T13:53:08Z" } }
      responses:
        "2XX": { description: Qualquer 2xx confirma a entrega. }
  message.edited:
    post:
      summary: Mensagem editada
      x-slug: message-edited
      x-litezap-milestone: M6
      x-zapi: Ao receber (mensagem editada)
      description: |
        Uma mensagem foi editada: pela API (`type: edit`), pelo celular ou pelo contato. Edições recebidas
        só valem do próprio autor; as demais são ignoradas. O log é atualizado (`edited_at` e texto novo).
        `id` vem vazio quando a mensagem original não está no log.
      x-related: [message.revoked, message.received]
      x-fields:
        - [id, string, "Id LiteZap da mensagem editada (opcional)."]
        - [wa_message_id, string, "Id no WhatsApp da mensagem editada."]
        - [chat, string, "JID do chat."]
        - [sender, string, "Quem editou (opcional)."]
        - [from_me, boolean, "Editada por este número."]
        - [source, string, "contact, api ou device."]
        - [text, string, "Texto novo."]
      requestBody:
        content:
          application/json:
            schema: { $ref: "#/components/schemas/Envelope" }
            example: { id: evt_7a2c9e4f1b6d3085, type: message.edited, version: 1, instance_id: inst_7f3a9c2e5b1d4086, timestamp: "2026-10-18T10:12:00.120Z", data: { id: msg_e29d4b8a0f6c1357, wa_message_id: 3A5D8F1B4E7C0A3D6F9B, chat: 5511999999999@s.whatsapp.net, sender: 5511999999999@s.whatsapp.net, from_me: false, source: contact, text: "Oi! Na verdade são 2 unidades.", timestamp: "2026-10-18T10:12:00Z" } }
      responses:
        "2XX": { description: Qualquer 2xx confirma a entrega. }
  message.revoked:
    post:
      summary: Mensagem apagada
      x-slug: message-revoked
      x-litezap-milestone: M6
      x-zapi: Ao receber (mensagem apagada)
      description: |
        Uma mensagem foi apagada para todos: pelo autor (pela API com `type: revoke`, pelo celular ou pelo
        contato) ou por um admin do grupo. O log marca `revoked_at`. `id` vem vazio quando a mensagem não
        está no log.
      x-related: [message.edited, message.status]
      x-fields:
        - [id, string, "Id LiteZap da mensagem apagada (opcional)."]
        - [wa_message_id, string, "Id no WhatsApp da mensagem apagada."]
        - [chat, string, "JID do chat."]
        - [sender, string, "Quem apagou (opcional)."]
        - [from_me, boolean, "Apagada por este número."]
        - [source, string, "contact, api ou device."]
      requestBody:
        content:
          application/json:
            schema: { $ref: "#/components/schemas/Envelope" }
            example: { id: evt_1d8b4f0a7c3e9256, type: message.revoked, version: 1, instance_id: inst_7f3a9c2e5b1d4086, timestamp: "2026-10-18T10:14:30.402Z", data: { id: msg_e29d4b8a0f6c1357, wa_message_id: 3A5D8F1B4E7C0A3D6F9B, chat: 5511999999999@s.whatsapp.net, sender: 5511999999999@s.whatsapp.net, from_me: false, source: contact, timestamp: "2026-10-18T10:14:30Z" } }
      responses:
        "2XX": { description: Qualquer 2xx confirma a entrega. }
  message.poll_vote:
    post:
      summary: Voto em enquete
      x-slug: message-poll-vote
      x-litezap-milestone: M6
      x-zapi: Ao receber (voto de enquete)
      description: |
        Alguém votou numa enquete. `selected_options` é o voto atual da pessoa (lista vazia = voto
        retirado), com as opções já decifradas pelo texto da enquete no log. Votos não ficam no log, só
        neste evento. Votos em enquetes criadas antes do pareamento não são decifrados (contam em
        `unknown_options`).
      x-related: [message.received]
      x-fields:
        - [poll_id, string, "Id LiteZap da enquete (opcional)."]
        - [poll_wa_message_id, string, "Id no WhatsApp da enquete."]
        - [chat, string, "JID do chat."]
        - [voter, string, "Quem votou."]
        - [push_name, string, "Nome de perfil de quem votou (opcional)."]
        - [from_me, boolean, "Voto deste número."]
        - [selected_options, array, "Opções escolhidas; vazia quando o voto foi retirado."]
        - [unknown_options, integer, "Opções que não puderam ser decifradas (opcional)."]
      requestBody:
        content:
          application/json:
            schema: { $ref: "#/components/schemas/Envelope" }
            example: { id: evt_9e3b7d1a5c0f4628, type: message.poll_vote, version: 1, instance_id: inst_7f3a9c2e5b1d4086, timestamp: "2026-10-18T11:02:15.880Z", data: { poll_id: msg_5c8e1a3f7b2d4069, poll_wa_message_id: 3EB0D2E4F6A8B0C1D3E5F7, chat: 5511999999999@s.whatsapp.net, voter: 5511999999999@s.whatsapp.net, push_name: Maria, from_me: false, selected_options: ["Terça às 14h"], timestamp: "2026-10-18T11:02:15Z" } }
      responses:
        "2XX": { description: Qualquer 2xx confirma a entrega. }
  history.synced:
    post:
      summary: Histórico sincronizado
      x-slug: history-synced
      x-litezap-milestone: M3
      x-zapi: (sem equivalente)
      description: |
        Depois do pareamento, o WhatsApp envia o histórico de conversas em blocos. O LiteZap grava tudo no log
        sem gerar um evento por mensagem e emite um único `history.synced` por sync, 15 segundos depois do
        último bloco. Bom momento para recarregar `GET /chats`.
      x-related: [instance.status]
      x-fields:
        - [chats, integer, "Blocos de histórico ingeridos (cerca de um por chat)."]
        - [messages, integer, "Mensagens nesses blocos."]
        - [new_messages, integer, "Mensagens que ainda não estavam no log."]
      requestBody:
        content:
          application/json:
            schema: { $ref: "#/components/schemas/Envelope" }
            example: { id: evt_5c1e8a4d0b7f2396, type: history.synced, version: 1, instance_id: inst_7f3a9c2e5b1d4086, timestamp: "2026-10-17T13:41:30.902Z", data: { chats: 48, messages: 3120, new_messages: 3120 } }
      responses:
        "2XX": { description: Qualquer 2xx confirma a entrega. }
  message.button_reply:
    post:
      summary: Resposta de botão ou lista
      x-slug: message-button-reply
      x-litezap-milestone: M7
      x-zapi: Responder botão / lista (recebimento)
      description: |
        Um contato respondeu a uma mensagem de botões, lista, template ou interativa (enviada por outra
        ferramenta: o LiteZap só recebe). A resposta fica no log com `type: button_reply`. `text` é o texto
        escolhido e `reply_to` a mensagem dos botões, quando o WhatsApp informa.
      x-related: [message.received]
      x-fields:
        - [button_reply.kind, string, "button, list, template ou interactive."]
        - [button_reply.id, string, "Id do botão ou da linha escolhida (interactive: nome do fluxo)."]
        - [button_reply.title, string, "Texto mostrado na escolha."]
        - [button_reply.description, string, "Descrição da linha (lista) ou parâmetros JSON (interactive) (opcional)."]
      requestBody:
        content:
          application/json:
            schema: { $ref: "#/components/schemas/Envelope" }
            example: { id: evt_3d9a1f7c5e2b8046, type: message.button_reply, version: 1, instance_id: inst_7f3a9c2e5b1d4086, timestamp: "2026-10-18T10:05:00.000Z", data: { id: msg_4b8e2d0a6c1f3957, wa_message_id: 3A9C2E5B1D4F7A0C3E6B, chat: 5511999999999@s.whatsapp.net, sender: 5511999999999@s.whatsapp.net, from_me: false, source: contact, type: button_reply, text: "Plano mensal", reply_to: 3EB0C4D5E6F7A8B9C0D1E2, button_reply: { kind: list, id: plano_mensal, title: "Plano mensal" }, timestamp: "2026-10-18T10:05:00Z" } }
      responses:
        "2XX": { description: Qualquer 2xx confirma a entrega. }
  chat.presence:
    post:
      summary: Presença no chat
      x-slug: chat-presence
      x-litezap-milestone: M7
      x-zapi: Status do chat
      description: |
        Contato digitando (`composing`), gravando áudio (`recording`) ou parando (`paused`) num chat, e
        contatos assinados com `POST /presence/subscribe` ficando `online`/`offline`. **Opt-in**: só chega
        com `presence.online` ligado nas settings (o WhatsApp só manda presença para contas online, e com
        isso o celular para de mostrar notificações) e só para webhooks/streams que pedem o tipo pelo nome
        (`chat.presence` ou `chat.*`; `*` não inclui). Repetições do mesmo estado em menos de 5 segundos
        são descartadas.
      x-related: [message.received]
      x-fields:
        - [chat, string, "JID do chat (ou do contato, em online/offline)."]
        - [sender, string, "Quem digita (o participante, em grupos)."]
        - [is_group, boolean, "Chat de grupo (opcional)."]
        - [state, string, "composing, recording, paused, online ou offline."]
        - [last_seen, string, "Em offline: visto por último, se o contato mostra (opcional)."]
      requestBody:
        content:
          application/json:
            schema: { $ref: "#/components/schemas/Envelope" }
            example: { id: evt_7e1b4d8a2c6f0935, type: chat.presence, version: 1, instance_id: inst_7f3a9c2e5b1d4086, timestamp: "2026-10-18T10:06:00.000Z", data: { chat: 5511999999999@s.whatsapp.net, sender: 5511999999999@s.whatsapp.net, state: composing } }
      responses:
        "2XX": { description: Qualquer 2xx confirma a entrega. }
  call.received:
    post:
      summary: Chamada recebida
      x-slug: call-received
      x-litezap-milestone: M7
      x-zapi: Webhook de chamadas
      description: |
        Uma chamada de voz ou vídeo está tocando (individual, ou em grupo com `is_group`). Com
        `calls.auto_reject` uma chamada individual é rejeitada antes do evento (em menos de 3 segundos)
        e sai com `rejected: true`; `calls.reject_message` vai ao autor pela fila. Chamadas em grupo e
        ofertas com mais de 1 minuto (entregues depois de uma reconexão) só são informadas. Atender chamadas pela API não é
        possível.
      x-related: [call.accepted, call.ended]
      x-fields:
        - [call_id, string, "Id da chamada."]
        - [chat, string, "Chat de quem ligou, ou o grupo."]
        - [caller, string, "JID de quem ligou."]
        - [is_group, boolean, "Chamada de grupo (opcional)."]
        - [media, string, "audio ou video (opcional)."]
        - [rejected, boolean, "Rejeitada automaticamente (opcional)."]
        - [timestamp, string, "Quando tocou."]
      requestBody:
        content:
          application/json:
            schema: { $ref: "#/components/schemas/Envelope" }
            example: { id: evt_1f6c9a3e7b0d2584, type: call.received, version: 1, instance_id: inst_7f3a9c2e5b1d4086, timestamp: "2026-10-18T10:07:00.000Z", data: { call_id: 4B2E9D1F7A3C, chat: 5511999999999@s.whatsapp.net, caller: 5511999999999@s.whatsapp.net, media: audio, rejected: true, timestamp: "2026-10-18T10:07:00Z" } }
      responses:
        "2XX": { description: Qualquer 2xx confirma a entrega. }
  call.accepted:
    post:
      summary: Chamada atendida
      x-slug: call-accepted
      x-litezap-milestone: M7
      x-zapi: Webhook de chamadas
      description: A chamada foi atendida num aparelho da conta (o celular, por exemplo).
      x-related: [call.received, call.ended]
      x-fields:
        - [call_id, string, "Id da chamada."]
        - [chat, string, "Chat de quem ligou, ou o grupo."]
        - [caller, string, "JID de quem ligou."]
        - [timestamp, string, "Quando foi atendida."]
      requestBody:
        content:
          application/json:
            schema: { $ref: "#/components/schemas/Envelope" }
            example: { id: evt_5a0e3c7f1d9b4628, type: call.accepted, version: 1, instance_id: inst_7f3a9c2e5b1d4086, timestamp: "2026-10-18T10:08:00.000Z", data: { call_id: 4B2E9D1F7A3C, chat: 5511999999999@s.whatsapp.net, caller: 5511999999999@s.whatsapp.net, media: video, timestamp: "2026-10-18T10:08:00Z" } }
      responses:
        "2XX": { description: Qualquer 2xx confirma a entrega. }
  call.ended:
    post:
      summary: Chamada encerrada
      x-slug: call-ended
      x-litezap-milestone: M7
      x-zapi: Webhook de chamadas
      description: |
        A chamada terminou; sai uma vez por chamada. `missed: true` quando tocou sem ninguém atender e
        não foi rejeitada. `reason`: `caller_hangup` (tocou sem atender até quem ligou desistir),
        `accepted_elsewhere` (atendida no celular ou em outro aparelho da conta; sai logo depois de
        `call.accepted`, no atendimento: o aparelho vinculado não é avisado do fim da conversa),
        `ended` (encerrada depois de atendida), `rejected` (por `calls.auto_reject`),
        `rejected_by_peer` (o outro lado rejeitou e nenhum fim chegou em 5 segundos), `timeout`
        (nenhum sinal em 2 minutos) ou outro motivo enviado pelo WhatsApp. O "reject" que um aparelho
        hospedado do WhatsApp (`hosted.lid`, aparelho 99) manda logo após toda oferta é ignorado.
      x-related: [call.received, call.accepted]
      x-fields:
        - [call_id, string, "Id da chamada."]
        - [chat, string, "Chat de quem ligou, ou o grupo."]
        - [caller, string, "JID de quem ligou."]
        - [reason, string, "Motivo do fim (opcional)."]
        - [missed, boolean, "Chamada perdida (opcional)."]
        - [timestamp, string, "Quando terminou."]
      requestBody:
        content:
          application/json:
            schema: { $ref: "#/components/schemas/Envelope" }
            example: { id: evt_9c4f2a8e6b1d3075, type: call.ended, version: 1, instance_id: inst_7f3a9c2e5b1d4086, timestamp: "2026-10-18T10:09:00.000Z", data: { call_id: 7D1A3F9B2E5C, chat: 5511999999999@s.whatsapp.net, caller: 5511999999999@s.whatsapp.net, media: audio, reason: timeout, missed: true, timestamp: "2026-10-18T10:09:00Z" } }
      responses:
        "2XX": { description: Qualquer 2xx confirma a entrega. }
  group.updated:
    post:
      summary: Grupo alterado
      x-slug: group-updated
      x-litezap-milestone: M8
      x-zapi: Webhook de recebimento (notificações de grupo)
      description: |
        Mudança num grupo em que o número está, feita no celular, por outro membro ou pela API. Sai um
        evento por tipo de mudança (`kind`): `participants_added` (`reason: invite` = entrou por link),
        `participants_removed`, `participants_promoted`, `participants_demoted`, `join_request` (pedido de
        entrada em grupo com aprovação), `join_request_revoked` (pedido cancelado ou recusado), `subject`
        (`name`), `description` (`""` = removida), `photo` (`removed`), `settings` (só os campos que
        mudaram; a aprovação de entrada sai depois, lida do grupo), `invite_link_reset`, `joined` (o número
        foi adicionado ou criou o grupo), `deleted`, `suspended`/`unsuspended`,
        `community_linked`/`community_unlinked` (`group`).
        `filters.chat: group` do webhook casa com ele.
      x-related: [message.received]
      x-fields:
        - [chat, string, "JID do grupo."]
        - [kind, string, "Tipo da mudança."]
        - [actor, string, "Quem fez a mudança (opcional)."]
        - [participants, array, "Participantes afetados (participants_*, join_request)."]
        - [reason, string, "participants_added/joined: invite quando por link (opcional)."]
        - [name, string, "subject, joined (opcional)."]
        - [description, string, "description (opcional)."]
        - [removed, boolean, "photo: foto removida (opcional)."]
        - [settings, object, "settings: announce, locked, approval_required, ephemeral_seconds (opcional)."]
        - [group, string, "community_linked/unlinked: o grupo (opcional)."]
        - [timestamp, string, "Quando aconteceu."]
      requestBody:
        content:
          application/json:
            schema: { $ref: "#/components/schemas/Envelope" }
            examples:
              participants:
                summary: Participante adicionado
                value: { id: evt_2b7d0f4a9c1e6358, type: group.updated, version: 1, instance_id: inst_7f3a9c2e5b1d4086, timestamp: "2026-10-18T11:00:00.000Z", data: { chat: 120363000000000001@g.us, kind: participants_added, actor: 5511999999999@s.whatsapp.net, participants: [5511888888888@s.whatsapp.net], timestamp: "2026-10-18T11:00:00Z" } }
              settings:
                summary: Configurações
                value: { id: evt_8e3a1c6f0b2d9475, type: group.updated, version: 1, instance_id: inst_7f3a9c2e5b1d4086, timestamp: "2026-10-18T11:01:00.000Z", data: { chat: 120363000000000001@g.us, kind: settings, actor: 5511999999999@s.whatsapp.net, settings: { announce: true }, timestamp: "2026-10-18T11:01:00Z" } }
      responses:
        "2XX": { description: Qualquer 2xx confirma a entrega. }
  chat.updated:
    post:
      summary: Chat alterado
      x-slug: chat-updated
      x-litezap-milestone: M9
      x-zapi: Webhook de chats
      description: |
        Um chat foi arquivado/desarquivado, fixado, silenciado, marcado como (não) lido, apagado ou limpo,
        ou uma mensagem foi favoritada, pela API (`source: "api"`) ou em qualquer aparelho da conta
        (`source: "device"`). Sai uma vez por mudança: o eco que o WhatsApp devolve das ações da API não
        duplica. `changed` diz o que mudou; os demais campos são o estado do chat depois da mudança.
      x-fields:
        - [chat, string, "JID do chat."]
        - [changed, array, "archived, pinned, muted, unread, deleted, cleared ou starred."]
        - [archived, boolean, "Arquivado."]
        - [pinned, boolean, "Fixado."]
        - [muted, boolean, "Silenciado."]
        - [muted_until, string, "Fim do silêncio (opcional; ausente = até reativar)."]
        - [unread, boolean, "Marcado como não lido."]
        - [starred, boolean, "starred: estado novo da mensagem (opcional)."]
        - [source, string, "api ou device."]
      requestBody:
        content:
          application/json:
            schema: { $ref: "#/components/schemas/Envelope" }
            example: { id: evt_1b7e5c3a9d2f4086, type: chat.updated, version: 1, instance_id: inst_7f3a9c2e5b1d4086, timestamp: "2026-10-18T10:10:00.000Z", data: { chat: 5511999999999@s.whatsapp.net, changed: [archived], archived: true, pinned: false, muted: false, unread: false, source: device, timestamp: "2026-10-18T10:10:00Z" } }
      responses:
        "2XX": { description: Qualquer 2xx confirma a entrega. }
  instance.proxy_failed:
    post:
      summary: Proxy da instância falhou
      x-slug: instance-proxy-failed
      x-litezap-milestone: M11
      x-zapi: Webhook de falha de proxy
      description: |
        Emitido quando a instância passa a não alcançar o WhatsApp pelo proxy dela (estado `reconnecting`
        com motivo `proxy_unreachable`) e repetido a cada 5 minutos enquanto a queda durar. A volta é o
        próximo `instance.status`. `error` traz o último erro de conexão do worker, sem usuário/senha do
        proxy, números de telefone nem tokens.
      x-related: [instance.status]
      x-fields:
        - [reason, string, "Sempre proxy_unreachable."]
        - [since, string, "Início da queda atual."]
        - [duration_seconds, integer, "Duração até agora."]
        - [outages_last_hour, integer, "Quedas iniciadas na última hora, contando esta."]
        - [repeat, integer, "0 no primeiro aviso; cresce a cada lembrete."]
        - [error, string, "Último erro de conexão (opcional, sem credenciais)."]
      requestBody:
        content:
          application/json:
            schema: { $ref: "#/components/schemas/Envelope" }
            example: { id: evt_3d9a1f7c5e2b4068, type: instance.proxy_failed, version: 1, instance_id: inst_7f3a9c2e5b1d4086, timestamp: "2026-10-18T15:12:44.020Z", data: { reason: proxy_unreachable, since: "2026-10-18T15:12:44Z", duration_seconds: 0, outages_last_hour: 1, repeat: 0, error: "socks connect tcp socks5://[redacted]@proxy.example:1080->web.whatsapp.com:443: connection refused" } }
      responses:
        "2XX": { description: Qualquer 2xx confirma a entrega. }
  instance.paired:
    post:
      summary: Pareamento concluído
      x-slug: instance-paired
      x-litezap-milestone: M11
      x-zapi: (sem equivalente; "Ao conectar" sem distinguir pareamento)
      description: |
        Emitido uma vez por pareamento: a instância estava esperando QR ou código e conectou. Reconexões não
        disparam este evento (use `instance.status`). `via_link` é `true` quando havia um link de pareamento
        válido — ele é consumido por este pareamento.
      x-related: [instance.status, instance.qr, instance.pair_code]
      x-fields:
        - [phone, string, "Número pareado (só dígitos)."]
        - [via_link, boolean, "Havia link de pareamento válido (agora consumido)."]
      requestBody:
        content:
          application/json:
            schema: { $ref: "#/components/schemas/Envelope" }
            example: { id: evt_8c2f6a0d4e1b3957, type: instance.paired, version: 1, instance_id: inst_7f3a9c2e5b1d4086, timestamp: "2026-10-18T16:58:03.120Z", data: { phone: "5511999999999", via_link: true } }
      responses:
        "2XX": { description: Qualquer 2xx confirma a entrega. }
  status.received:
    post:
      summary: Status de um contato
      x-slug: status-received
      x-litezap-milestone: M10
      x-zapi: Webhook de status
      description: |
        Um contato publicou um status (story), recebido com a instância conectada. **Opt-in**: só sai
        para quem pede o tipo explicitamente (`status.received` ou `status.*`); `*` não inclui. O status
        fica no log (`GET /status`) e a mídia baixa por `media.url`.
      x-related: [message.received]
      x-fields:
        - [id, string, "Id do LiteZap (use em /status/{msg_id}/reply)."]
        - [chat, string, "Sempre status@broadcast."]
        - [sender, string, "Autor do status."]
        - [type, string, "text, image, video..."]
        - [text, string, "Texto ou legenda (opcional)."]
        - [media, object, "Mídia com url de download (opcional)."]
      requestBody:
        content:
          application/json:
            schema: { $ref: "#/components/schemas/Envelope" }
            example: { id: evt_4d2a8c6e0b1f3957, type: status.received, version: 1, instance_id: inst_7f3a9c2e5b1d4086, timestamp: "2026-10-18T12:30:00.000Z", data: { id: msg_7a3c1e9d5b2f4086, wa_message_id: 3A1B2C3D4E5F6A7B8C9D, chat: status@broadcast, sender: 5511988887777@s.whatsapp.net, from_me: false, push_name: Ana, source: contact, type: image, text: "Praia hoje", media: { id: med_2c8e4a6f0b1d3957, kind: image, mime: image/jpeg, url: /v1/instances/inst_7f3a9c2e5b1d4086/media/med_2c8e4a6f0b1d3957 }, timestamp: "2026-10-18T12:30:00Z" } }
      responses:
        "2XX": { description: Qualquer 2xx confirma a entrega. }
  channel.message:
    post:
      summary: Publicação em canal seguido
      x-slug: channel-message
      x-litezap-milestone: M10
      x-zapi: (sem equivalente)
      description: |
        Publicação nova num canal que o número segue. **Opt-in** como `status.received`. Não fica no log e a
        mídia vem só com metadados.
      x-fields:
        - [channel, string, "JID do canal."]
        - [server_id, integer, "Número da publicação (use para reagir)."]
        - [type, string, "Tipo."]
        - [text, string, "Texto ou legenda (opcional)."]
      requestBody:
        content:
          application/json:
            schema: { $ref: "#/components/schemas/Envelope" }
            example: { id: evt_9b1d3f5a7c2e4068, type: channel.message, version: 1, instance_id: inst_7f3a9c2e5b1d4086, timestamp: "2026-10-18T12:31:00.000Z", data: { channel: 120363000000000002@newsletter, server_id: 812, wa_message_id: "3EB0123456789ABCDEF0", type: text, text: "Manchete do dia", timestamp: "2026-10-18T12:31:00Z" } }
      responses:
        "2XX": { description: Qualquer 2xx confirma a entrega. }
  label.updated:
    post:
      summary: Etiqueta alterada
      x-slug: label-updated
      x-litezap-milestone: M10
      x-zapi: (sem equivalente)
      description: |
        Etiqueta do WhatsApp Business criada, renomeada, recolorida ou apagada (`kind: edit`), ou aplicada/
        removida de uma conversa (`chat`) ou mensagem (`message`), pela API (`source: api`) ou no celular
        (`source: device`). O sync completo logo após parear grava as etiquetas sem gerar eventos.
      x-fields:
        - [kind, string, "edit, chat ou message."]
        - [label_id, string, "Id da etiqueta."]
        - [name, string, "edit: nome (opcional)."]
        - [color, integer, "edit: índice da cor (opcional)."]
        - [deleted, boolean, "edit: apagada (opcional)."]
        - [chat, string, "chat/message: a conversa (opcional)."]
        - [labeled, boolean, "chat/message: estado novo (opcional)."]
        - [source, string, "api ou device."]
      requestBody:
        content:
          application/json:
            schema: { $ref: "#/components/schemas/Envelope" }
            example: { id: evt_3e7a1c9d5b2f8046, type: label.updated, version: 1, instance_id: inst_7f3a9c2e5b1d4086, timestamp: "2026-10-18T12:32:00.000Z", data: { kind: chat, label_id: "5", chat: 5511988887777@s.whatsapp.net, labeled: true, source: device, timestamp: "2026-10-18T12:32:00Z" } }
      responses:
        "2XX": { description: Qualquer 2xx confirma a entrega. }
  agent.failed:
    post:
      summary: Atendente IA falhou
      x-slug: agent-failed
      x-litezap-milestone: M5
      x-zapi: (sem equivalente)
      description: |
        Uma chamada do hook de agente falhou e nada foi enviado (falha fechada), ou a proteção contra loop pausou
        o chat. Motivos do agente externo: `timeout`, `http_error`, `network_error`, `invalid_response`,
        `send_failed`, `loop_guard`, `internal`. Do Atendente LiteZap: também `not_configured`, `daily_cap`
        (limite diário de chamadas atingido) e `llm_error` (o provedor falhou).
      x-related: [agent.handoff]
      x-fields:
        - [chat, string, "Chat da mensagem."]
        - [message_id, string, "Id LiteZap da mensagem (ausente no follow-up de ligação)."]
        - [reason, string, "Código estável do motivo."]
        - [error, string, "Detalhe (opcional, sem chaves)."]
      requestBody:
        content:
          application/json:
            schema: { $ref: "#/components/schemas/Envelope" }
            example: { id: evt_5f1c9a3e7d2b4086, type: agent.failed, version: 1, instance_id: inst_7f3a9c2e5b1d4086, timestamp: "2026-10-18T19:02:11.020Z", data: { chat: "5511999999999@s.whatsapp.net", message_id: msg_1a2b3c4d5e6f7a8b, reason: daily_cap, error: "daily limit of 300 provider calls reached (daily_call_cap)" } }
      responses:
        "2XX": { description: Qualquer 2xx confirma a entrega. }
  agent.handoff:
    post:
      summary: Atendente passou para humano
      x-slug: agent-handoff
      x-litezap-milestone: M5
      x-zapi: (sem equivalente)
      description: O agente passou o chat para uma pessoa; ele fica desligado no chat até ser reativado.
      x-related: [agent.failed]
      x-fields:
        - [chat, string, "Chat."]
        - [message_id, string, "Mensagem que disparou (opcional)."]
        - [note, string, "Nota do agente para a equipe (opcional)."]
      requestBody:
        content:
          application/json:
            schema: { $ref: "#/components/schemas/Envelope" }
            example: { id: evt_7a3c1e9f5d2b4068, type: agent.handoff, version: 1, instance_id: inst_7f3a9c2e5b1d4086, timestamp: "2026-10-18T19:05:40.100Z", data: { chat: "5511999999999@s.whatsapp.net", message_id: msg_1a2b3c4d5e6f7a8b, note: "cliente quer negociar desconto" } }
      responses:
        "2XX": { description: Qualquer 2xx confirma a entrega. }
  agent.lead:
    post:
      summary: Lead qualificado pelo atendente
      x-slug: agent-lead
      x-litezap-milestone: M5b
      x-zapi: (sem equivalente)
      description: |
        O Atendente LiteZap (ação `lead` permitida, ex.: modo SDR ou vendedor) entendeu a necessidade do
        cliente. Leve para o CRM com um webhook ou n8n. Repetições idênticas no mesmo chat em 24h não são
        reemitidas. O telefone não passa pela IA: use `chat`.
      x-related: [agent.appointment_requested, agent.order_draft]
      x-fields:
        - [chat, string, "Chat do cliente."]
        - [message_id, string, "Mensagem que disparou."]
        - [agent_id, string, "Agente que atendeu."]
        - [agent_name, string, "Nome do agente."]
        - [mode, string, "Modo do agente."]
        - [customer_name, string, "Nome informado (pode ser vazio)."]
        - [need, string, "Necessidade do cliente."]
        - [notes, string, "Observações."]
      requestBody:
        content:
          application/json:
            schema: { $ref: "#/components/schemas/Envelope" }
            example: { id: evt_2b8d4f0a6c1e3957, type: agent.lead, version: 1, instance_id: inst_7f3a9c2e5b1d4086, timestamp: "2026-10-18T19:10:00.000Z", data: { chat: "5511999999999@s.whatsapp.net", message_id: msg_1a2b3c4d5e6f7a8b, agent_id: agt_3c9e1a7f5d2b4086, agent_name: Sara, mode: sdr, customer_name: Rui, need: "limpeza de pele", notes: "" } }
      responses:
        "2XX": { description: Qualquer 2xx confirma a entrega. }
  agent.appointment_requested:
    post:
      summary: Pedido de agendamento
      x-slug: agent-appointment-requested
      x-litezap-milestone: M5b
      x-zapi: (sem equivalente)
      description: |
        O Atendente LiteZap (modo SDR/agendamento) coletou nome, necessidade e horário de preferência. Nenhum
        calendário é tocado: confirme pela sua agenda (webhook, n8n) e responda o cliente.
      x-related: [agent.lead]
      x-fields:
        - [chat, string, "Chat do cliente."]
        - [message_id, string, "Mensagem que disparou."]
        - [agent_id, string, "Agente."]
        - [customer_name, string, "Nome."]
        - [need, string, "Necessidade."]
        - [preferred_time, string, "Dia e horário como o cliente disse (com data ISO quando clara)."]
        - [notes, string, "Observações."]
      requestBody:
        content:
          application/json:
            schema: { $ref: "#/components/schemas/Envelope" }
            example: { id: evt_9d3f5b1a7e2c4068, type: agent.appointment_requested, version: 1, instance_id: inst_7f3a9c2e5b1d4086, timestamp: "2026-10-18T19:12:00.000Z", data: { chat: "5511999999999@s.whatsapp.net", message_id: msg_1a2b3c4d5e6f7a8b, agent_id: agt_3c9e1a7f5d2b4086, agent_name: Sara, mode: sdr, customer_name: Rui, need: "limpeza de pele", preferred_time: "terça 15h (2026-10-20)", notes: "" } }
      responses:
        "2XX": { description: Qualquer 2xx confirma a entrega. }
  agent.order_draft:
    post:
      summary: Rascunho de pedido
      x-slug: agent-order-draft
      x-litezap-milestone: M5b
      x-zapi: (sem equivalente)
      description: |
        O Atendente LiteZap (modo facilitador de pedido) montou o pedido e pediu confirmação ao cliente. Os preços
        unitários vêm do catálogo do agente quando o nome bate (`in_catalog: true`) e o total é recalculado pelo
        motor; itens fora do catálogo vêm com o preço citado pela IA (`all_items_in_catalog: false`: confira).
        A confirmação do cliente é a próxima mensagem dele.
      x-related: [agent.lead]
      x-fields:
        - [chat, string, "Chat do cliente."]
        - [items, array, "name, quantity, unit_price, subtotal, in_catalog."]
        - [total, number, "Total recalculado."]
        - [currency, string, "Moeda do agente (BRL)."]
        - [all_items_in_catalog, boolean, "Todos os itens casaram com o catálogo."]
        - [address, string, "Endereço (ou retirada)."]
        - [notes, string, "Observações."]
      requestBody:
        content:
          application/json:
            schema: { $ref: "#/components/schemas/Envelope" }
            example: { id: evt_4e0a2c8f6b1d3957, type: agent.order_draft, version: 1, instance_id: inst_7f3a9c2e5b1d4086, timestamp: "2026-10-18T19:20:00.000Z", data: { chat: "5511999999999@s.whatsapp.net", message_id: msg_1a2b3c4d5e6f7a8b, agent_id: agt_5b1d3f9a7c2e4086, agent_name: Bia, mode: pedido, items: [{ name: Pizza Margherita, quantity: 2, unit_price: 45, subtotal: 90, in_catalog: true }], total: 90, currency: BRL, all_items_in_catalog: true, address: "Rua B, 5", notes: "sem cebola", customer_name: Ana } }
      responses:
        "2XX": { description: Qualquer 2xx confirma a entrega. }
