{
  "openapi": "3.1.0",
  "info": {
    "title": "API da VENUM",
    "version": "1.0.1",
    "summary": "Atendimento, leads, pedidos e mensagens pelo WhatsApp (API Oficial da Meta) para integrar com o sistema da empresa.",
    "description": "Tudo em JSON, sobre HTTPS. Uma chave de API (`vnm_…`, gerada no painel em Integrações) dá acesso a UMA conta. Chave somente leitura responde apenas aos GET; para enviar mensagem ou mover lead é preciso uma chave de acesso total. Erros vêm como `{ \"erro\": \"codigo_estavel\", \"mensagem\": \"texto para a pessoa\" }`. Valores monetários nas listas (`valor`) chegam como texto decimal (ex.: `\"249.90\"`). Documentação: https://venum.app.br/desenvolvedores/ · Privacidade: https://venum.app.br/privacidade/ · Índice para agentes: https://venum.app.br/llms.txt",
    "contact": { "name": "VENUM (Cobra · Tech Provider da Meta)", "url": "https://venum.app.br/contato/", "email": "contato@cobra.art.br" },
    "x-logo": { "url": "https://venum.app.br/images/logo-256.webp" }
  },
  "externalDocs": { "description": "Guia para desenvolvedores", "url": "https://venum.app.br/desenvolvedores/" },
  "servers": [{ "url": "https://dash.venum.app.br", "description": "Produção" }],
  "security": [{ "chaveVenum": [] }],
  "tags": [
    { "name": "Conta", "description": "Quem sou eu: confirma a chave e mostra a conta." },
    { "name": "Contatos", "description": "Pessoas que conversaram com a empresa e onde estão no funil." },
    { "name": "Pedidos", "description": "Vendas registradas no painel, com totais." },
    { "name": "Mensagens", "description": "Envio pelo WhatsApp da conta (texto na janela de 24h ou template aprovado)." },
    { "name": "Leads", "description": "Funil: mover o lead de estágio a partir do sistema da empresa." }
  ],
  "paths": {
    "/api/v1/conta": {
      "get": {
        "tags": ["Conta"],
        "operationId": "verConta",
        "summary": "Confirma a chave e mostra a conta",
        "description": "Primeira chamada de qualquer integração: valida a chave e diz a qual conta ela dá acesso e com qual escopo.",
        "responses": {
          "200": {
            "description": "Conta da chave",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/RespostaConta" },
              "example": { "conta": { "id": 12, "nome": "Loja Exemplo", "status": "connected", "numero": "+55 47 9999-0000", "modo": "completo", "contatos": 1480, "departamentos": 3 }, "escopo": "total" } } }
          },
          "401": { "$ref": "#/components/responses/NaoAutorizado" }
        }
      }
    },
    "/api/v1/contatos": {
      "get": {
        "tags": ["Contatos"],
        "operationId": "listarContatos",
        "summary": "Lista contatos da conta",
        "description": "Ordenados da última mensagem mais recente para a mais antiga. `janela_aberta` diz se o cliente escreveu nas últimas 24h (texto livre é entregue) ou não (só template aprovado).",
        "parameters": [
          { "name": "estagio", "in": "query", "description": "Filtra pelo estágio do funil. `sem_resposta` ainda não é aceito por este filtro (contatos nesse estágio aparecem sem filtro, com `estagio: \"sem_resposta\"`).", "schema": { "type": "string", "enum": ["novo", "atendimento", "qualificado", "adiado", "ganho", "perdido"] } },
          { "name": "desde", "in": "query", "description": "Só quem falou a partir desta data (AAAA-MM-DD).", "schema": { "type": "string", "format": "date" } },
          { "name": "busca", "in": "query", "description": "Trecho do nome ou do telefone.", "schema": { "type": "string" } },
          { "$ref": "#/components/parameters/limite" },
          { "$ref": "#/components/parameters/pagina" }
        ],
        "responses": {
          "200": { "description": "Página de contatos", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/RespostaContatos" } } } },
          "400": { "$ref": "#/components/responses/PedidoInvalido" },
          "401": { "$ref": "#/components/responses/NaoAutorizado" }
        }
      }
    },
    "/api/v1/pedidos": {
      "get": {
        "tags": ["Pedidos"],
        "operationId": "listarPedidos",
        "summary": "Vendas registradas, com totais",
        "description": "O que o financeiro puxa para conciliar. `ate` inclui o dia inteiro.",
        "parameters": [
          { "name": "desde", "in": "query", "schema": { "type": "string", "format": "date" }, "description": "A partir desta data (AAAA-MM-DD)." },
          { "name": "ate", "in": "query", "schema": { "type": "string", "format": "date" }, "description": "Até esta data, inclusive (AAAA-MM-DD)." },
          { "name": "status", "in": "query", "schema": { "type": "string", "enum": ["recebido", "a_receber"] } },
          { "$ref": "#/components/parameters/limite" },
          { "$ref": "#/components/parameters/pagina" }
        ],
        "responses": {
          "200": { "description": "Página de pedidos com resumo", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/RespostaPedidos" } } } },
          "400": { "$ref": "#/components/responses/PedidoInvalido" },
          "401": { "$ref": "#/components/responses/NaoAutorizado" }
        }
      }
    },
    "/api/v1/mensagens": {
      "post": {
        "tags": ["Mensagens"],
        "operationId": "enviarMensagem",
        "summary": "Envia mensagem pelo WhatsApp da conta",
        "description": "Exige chave de acesso total. Dentro da janela de 24h (o cliente escreveu recentemente) pode ir `texto`; fora dela o WhatsApp só entrega `template` aprovado. Se vierem os dois, vale o `template`. Se o telefone ainda não for um contato, ele é criado e o webhook `contato.criado` é emitido. Sucesso emite `mensagem.enviada`.",
        "requestBody": {
          "required": true,
          "content": { "application/json": { "schema": { "$ref": "#/components/schemas/NovaMensagem" },
            "examples": {
              "texto": { "summary": "Texto na janela de 24h", "value": { "telefone": "5547999990000", "texto": "Seu pedido saiu para entrega!" } },
              "template": { "summary": "Template aprovado (fora da janela)", "value": { "telefone": "5547999990000", "template": { "nome": "aviso_envio", "idioma": "pt_BR", "variaveis": ["Maria", "BR123456789"] } } }
            } } }
        },
        "responses": {
          "200": { "description": "Mensagem aceita pela Meta", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/RespostaEnvio" }, "example": { "enviada": true, "mensagem_id": "wamid.HBgL…", "contato_id": 1234 } } } },
          "400": { "$ref": "#/components/responses/PedidoInvalido" },
          "401": { "$ref": "#/components/responses/NaoAutorizado" },
          "403": { "$ref": "#/components/responses/SemPermissao" },
          "409": { "description": "O WhatsApp desta conta não está conectado no painel.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Erro" }, "example": { "erro": "conta_desconectada", "mensagem": "O WhatsApp desta conta não está conectado no painel." } } } },
          "422": { "description": "A Meta recusou o envio. A `mensagem` traz o erro da Meta e, quando é o caso, a dica da janela de 24h.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Erro" }, "example": { "erro": "falha_no_envio", "mensagem": "(#131047) Re-engagement message Passadas 24h da última mensagem do cliente, só template aprovado é entregue." } } } }
        }
      }
    },
    "/api/v1/leads/{id}": {
      "patch": {
        "tags": ["Leads"],
        "operationId": "moverLead",
        "summary": "Move o lead no funil",
        "description": "Exige chave de acesso total. Passa pelo MESMO caminho da tela: `ganho` com `valor` maior que zero cria o pedido, dispara a API de Conversões da Meta, escreve na linha do tempo e emite os webhooks `lead.mudou_estagio` e `pedido.criado`. `ganho` sem valor (ou com 0) só muda o estágio (`pedido_id: null`). Valor negativo é recusado com 400 `valor_invalido`.",
        "parameters": [{ "name": "id", "in": "path", "required": true, "description": "Id do contato/lead nesta conta.", "schema": { "type": "integer" } }],
        "requestBody": {
          "required": true,
          "content": { "application/json": { "schema": { "$ref": "#/components/schemas/MudancaDeEstagio" },
            "examples": {
              "ganho": { "value": { "estagio": "ganho", "valor": 249.9, "produto": "Plano anual", "forma_pagamento": "pix" } },
              "adiado": { "value": { "estagio": "adiado", "retomar_em": "2026-12-01", "retomar_motivo": "13º" } },
              "perdido": { "value": { "estagio": "perdido", "motivo_perda": "Comprou do concorrente" } }
            } } }
        },
        "responses": {
          "200": { "description": "Lead movido", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/RespostaLead" }, "example": { "ok": true, "lead_id": 1234, "estagio": "ganho", "pedido_id": 88, "retomar_em": null, "capi": { "enviada": true, "motivo": null } } } } },
          "400": { "$ref": "#/components/responses/PedidoInvalido" },
          "401": { "$ref": "#/components/responses/NaoAutorizado" },
          "403": { "$ref": "#/components/responses/SemPermissao" },
          "404": { "description": "Não existe lead com esse id nesta conta.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Erro" }, "example": { "erro": "nao_encontrado", "mensagem": "Não existe lead com esse id nesta conta." } } } }
        }
      }
    }
  },
  "webhooks": {
    "mensagem.recebida": { "post": { "summary": "O cliente mandou mensagem", "description": "É assim que um contato novo vindo do WhatsApp aparece para a integração.", "requestBody": { "$ref": "#/components/requestBodies/EntregaWebhook" }, "responses": { "2XX": { "$ref": "#/components/responses/WebhookRecebido" } } } },
    "mensagem.enviada": { "post": { "summary": "Alguém — ou a IA — respondeu", "requestBody": { "$ref": "#/components/requestBodies/EntregaWebhook" }, "responses": { "2XX": { "$ref": "#/components/responses/WebhookRecebido" } } } },
    "contato.criado": { "post": { "summary": "A própria API criou o contato", "description": "Emitido quando `POST /api/v1/mensagens` é chamado para um telefone que ainda não era contato (`dados.origem: \"api\"`). Contato novo que escreve pelo WhatsApp chega via `mensagem.recebida`.", "requestBody": { "$ref": "#/components/requestBodies/EntregaWebhook" }, "responses": { "2XX": { "$ref": "#/components/responses/WebhookRecebido" } } } },
    "lead.mudou_estagio": { "post": { "summary": "O lead andou no funil (inclui ganho e perdido)", "requestBody": { "$ref": "#/components/requestBodies/EntregaWebhook" }, "responses": { "2XX": { "$ref": "#/components/responses/WebhookRecebido" } } } },
    "pedido.criado": { "post": { "summary": "Uma venda foi registrada, com valor", "requestBody": { "$ref": "#/components/requestBodies/EntregaWebhook" }, "responses": { "2XX": { "$ref": "#/components/responses/WebhookRecebido" } } } },
    "agendamento.criado": { "post": { "summary": "Reservado — ainda não é emitido", "description": "Nome reservado para quando um horário for marcado. A plataforma ainda NÃO emite este evento; pode ser assinado, mas nada chega.", "x-status": "reservado", "requestBody": { "$ref": "#/components/requestBodies/EntregaWebhook" }, "responses": { "2XX": { "$ref": "#/components/responses/WebhookRecebido" } } } },
    "agendamento.cancelado": { "post": { "summary": "Reservado — ainda não é emitido", "description": "Nome reservado para quando um horário for desmarcado. A plataforma ainda NÃO emite este evento.", "x-status": "reservado", "requestBody": { "$ref": "#/components/requestBodies/EntregaWebhook" }, "responses": { "2XX": { "$ref": "#/components/responses/WebhookRecebido" } } } }
  },
  "components": {
    "securitySchemes": {
      "chaveVenum": { "type": "http", "scheme": "bearer", "bearerFormat": "vnm_…", "description": "Chave gerada no painel (Integrações). `Authorization: Bearer vnm_SUACHAVE`. Escopos: `leitura` (só GET) ou `total`." }
    },
    "parameters": {
      "limite": { "name": "limite", "in": "query", "description": "Itens por página (1–200).", "schema": { "type": "integer", "minimum": 1, "maximum": 200, "default": 50 } },
      "pagina": { "name": "pagina", "in": "query", "description": "Página, a partir de 1.", "schema": { "type": "integer", "minimum": 1, "default": 1 } }
    },
    "requestBodies": {
      "EntregaWebhook": {
        "description": "Entrega assinada, enviada com `User-Agent: Venum-Webhook/1` e tempo limite de 10 segundos. Cabeçalhos: `X-Venum-Event` (nome do evento), `X-Venum-Delivery` (id da entrega), `X-Venum-Signature` (`t=<unix>,v1=<hmac-sha256 hex>` de `t + \".\" + corpo cru` com o segredo do endpoint; recuse se `t` tiver mais de 5 minutos). Responda com qualquer 2xx; outra resposta ou tempo esgotado gera nova tentativa em 5, 30, 120 e 360 minutos (5 tentativas no total; depois a entrega fica como `falhou` no painel).",
        "required": true,
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/EventoWebhook" },
          "example": { "evento": "lead.mudou_estagio", "criado_em": "2026-08-24T18:00:00.000Z", "conta_id": 12, "dados": { "contato_id": 1234, "estagio": "ganho", "rotulo": "Ganho", "valor": 249.9, "pedido_id": 88, "capi_enviada": true } } } }
      }
    },
    "responses": {
      "WebhookRecebido": { "description": "Recebido (qualquer 2xx conta como entregue)." },
      "NaoAutorizado": { "description": "Chave ausente, errada ou revogada.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Erro" }, "example": { "erro": "nao_autorizado", "mensagem": "Chave de API ausente ou inválida. Envie no cabeçalho: Authorization: Bearer vnm_..." } } } },
      "SemPermissao": { "description": "A chave é somente leitura e a rota escreve.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Erro" }, "example": { "erro": "sem_permissao", "mensagem": "Esta chave é somente leitura. Gere uma chave de acesso total para escrever." } } } },
      "PedidoInvalido": { "description": "Pedido malformado — a mensagem diz o que corrigir (ex.: `estagio_invalido`, `data_invalida`, `telefone_obrigatorio`, `telefone_invalido`, `conteudo_obrigatorio`, `id_invalido`, `valor_invalido`, `status_invalido`).", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Erro" }, "example": { "erro": "estagio_invalido", "mensagem": "Estágio \"fechado\" não existe. Use: novo, atendimento, qualificado, adiado, ganho, perdido." } } } }
    },
    "schemas": {
      "Erro": { "type": "object", "required": ["erro", "mensagem"], "properties": { "erro": { "type": "string", "description": "Código estável — trate por ele." }, "mensagem": { "type": "string", "description": "Texto em português para mostrar à pessoa." } } },
      "Estagio": { "type": "string", "enum": ["novo", "atendimento", "qualificado", "adiado", "sem_resposta", "ganho", "perdido"] },
      "ValorDecimal": { "type": ["string", "null"], "pattern": "^-?[0-9]+(\\.[0-9]+)?$", "description": "Valor em reais como texto decimal com ponto (ex.: \"249.90\"). Converta com Number()/Decimal.", "examples": ["249.90"] },
      "RespostaConta": {
        "type": "object",
        "properties": {
          "conta": { "type": "object", "properties": {
            "id": { "type": "integer" }, "nome": { "type": "string" }, "status": { "type": "string", "description": "`connected` quando o WhatsApp está conectado." },
            "numero": { "type": ["string", "null"], "description": "Número do WhatsApp, formatado." }, "modo": { "type": ["string", "null"], "description": "Forma de uso da conta (painel completo, roteador, só qualificar)." },
            "contatos": { "type": "integer" }, "departamentos": { "type": "integer" } } },
          "escopo": { "type": "string", "enum": ["leitura", "total"] }
        }
      },
      "Contato": {
        "type": "object",
        "properties": {
          "id": { "type": "integer" }, "telefone": { "type": "string", "description": "Com DDI e DDD, só dígitos. Ex.: 5547999990000." }, "nome": { "type": ["string", "null"] },
          "estagio": { "$ref": "#/components/schemas/Estagio" }, "valor": { "$ref": "#/components/schemas/ValorDecimal" }, "forma_pagamento": { "type": ["string", "null"] },
          "ultima_mensagem_em": { "type": ["string", "null"], "format": "date-time" }, "ultima_do_cliente_em": { "type": ["string", "null"], "format": "date-time" },
          "retomar_em": { "type": ["string", "null"], "format": "date-time", "description": "Data marcada para retomar (chega como date-time ISO, meia-noite no fuso do servidor; o PATCH e o webhook usam AAAA-MM-DD)." },
          "retomar_motivo": { "type": ["string", "null"] }, "motivo_perda": { "type": ["string", "null"] },
          "veio_de_anuncio": { "type": "boolean", "description": "Chegou por um anúncio click-to-WhatsApp da Meta." }, "anuncio": { "type": ["string", "null"], "description": "Título do anúncio de origem." },
          "departamento": { "type": ["string", "null"] }, "criado_em": { "type": "string", "format": "date-time" },
          "janela_aberta": { "type": "boolean", "description": "true se o cliente escreveu nas últimas 24h (texto livre é entregue)." }
        }
      },
      "Paginacao": { "type": "object", "properties": { "pagina": { "type": "integer" }, "limite": { "type": "integer" }, "total": { "type": "integer" } } },
      "RespostaContatos": { "type": "object", "properties": { "contatos": { "type": "array", "items": { "$ref": "#/components/schemas/Contato" } }, "paginacao": { "$ref": "#/components/schemas/Paginacao" } } },
      "Pedido": {
        "type": "object",
        "properties": {
          "id": { "type": "integer" }, "valor": { "$ref": "#/components/schemas/ValorDecimal" }, "status": { "type": "string", "enum": ["recebido", "a_receber"] }, "forma_pagamento": { "type": ["string", "null"] },
          "produto": { "type": ["string", "null"] }, "criado_em": { "type": "string", "format": "date-time" },
          "contato_id": { "type": "integer" }, "telefone": { "type": "string" }, "nome": { "type": ["string", "null"] }
        }
      },
      "RespostaPedidos": {
        "type": "object",
        "properties": {
          "pedidos": { "type": "array", "items": { "$ref": "#/components/schemas/Pedido" } },
          "resumo": { "type": "object", "properties": { "quantidade": { "type": "integer" }, "valor_total": { "type": "number" }, "recebido": { "type": "number" }, "a_receber": { "type": "number" } }, "description": "Totais já convertidos para número." },
          "paginacao": { "$ref": "#/components/schemas/Paginacao" }
        }
      },
      "NovaMensagem": {
        "type": "object",
        "required": ["telefone"],
        "anyOf": [{ "required": ["texto"] }, { "required": ["template"] }],
        "properties": {
          "telefone": { "type": "string", "description": "Com DDI e DDD (12–15 dígitos). Ex.: 5547999990000." },
          "nome": { "type": "string", "maxLength": 80, "description": "Usado só se o contato ainda não existir." },
          "texto": { "type": "string", "maxLength": 4000, "description": "Texto livre — só é entregue dentro da janela de 24h." },
          "template": { "type": "object", "required": ["nome"], "properties": { "nome": { "type": "string", "description": "Nome do template aprovado pela Meta." }, "idioma": { "type": "string", "default": "pt_BR" }, "variaveis": { "type": "array", "items": { "type": "string" }, "description": "Valores de {{1}}, {{2}}… do corpo do template." } } }
        },
        "description": "Envie `texto` OU `template` (um dos dois é obrigatório — sem nenhum, 400 `conteudo_obrigatorio`). Com os dois, vale o `template`."
      },
      "RespostaEnvio": { "type": "object", "properties": { "enviada": { "type": "boolean" }, "mensagem_id": { "type": "string" }, "contato_id": { "type": "integer" } } },
      "MudancaDeEstagio": {
        "type": "object",
        "required": ["estagio"],
        "properties": {
          "estagio": { "$ref": "#/components/schemas/Estagio" },
          "valor": { "oneOf": [{ "type": "number", "minimum": 0 }, { "type": "string" }], "description": "Valor do pedido em `ganho` (aceita 249.90 ou \"R$ 249,90\"). Negativo → 400 `valor_invalido`; só valor > 0 cria pedido." },
          "forma_pagamento": { "type": "string" }, "status_pagamento": { "type": "string", "enum": ["recebido", "a_receber"] }, "produto": { "type": "string" },
          "motivo_perda": { "type": "string", "description": "Para `perdido`." },
          "retomar_em": { "type": "string", "format": "date", "description": "Para `adiado` (AAAA-MM-DD)." }, "retomar_motivo": { "type": "string" }
        }
      },
      "RespostaLead": {
        "type": "object",
        "properties": {
          "ok": { "type": "boolean" }, "lead_id": { "type": "integer" }, "estagio": { "$ref": "#/components/schemas/Estagio" },
          "pedido_id": { "type": ["integer", "null"] }, "retomar_em": { "type": ["string", "null"], "format": "date" },
          "capi": { "type": ["object", "null"], "description": "Resultado da API de Conversões da Meta, quando houve tentativa.", "properties": { "enviada": { "type": "boolean" }, "motivo": { "type": ["string", "null"], "description": "Por que não saiu (ex.: lead não veio de anúncio)." } } }
        }
      },
      "EventoWebhook": {
        "type": "object",
        "required": ["evento", "criado_em", "conta_id", "dados"],
        "properties": {
          "evento": { "type": "string", "enum": ["mensagem.recebida", "mensagem.enviada", "contato.criado", "lead.mudou_estagio", "pedido.criado", "agendamento.criado", "agendamento.cancelado"] },
          "criado_em": { "type": "string", "format": "date-time" },
          "conta_id": { "type": "integer" },
          "dados": { "type": "object", "additionalProperties": true, "description": "Conteúdo do evento (ex.: contato_id, telefone, estagio, valor, pedido_id, capi_enviada, mensagem_id, texto, origem)." }
        }
      }
    }
  }
}
