Nevoa Manager

Documentação para desenvolvedores

Public API v1

Receba uma cópia das conversas via webhook e manipule conversas, contatos, tags e setores pela API REST.

Visão geral

Webhook de saída

A Névoa faz POST no seu endpoint com mensagens e eventos de ciclo de vida, assinados com HMAC.

API REST

Endpoints em /api/public/v1 autenticados com Authorization: Bearer <apiToken>.

Escopo por número

Vincule integrações META_WHATSAPP em linkedIntegrationIds. Todo payload de conversa inclui o objeto channel.

Publicações Meta

Crie, agende, reagende, pause e publique posts no Facebook e Instagram em /social, acompanhando cada etapa pelos webhooks social_post.*. Exige vincular integrações Meta (linkedMetaIntegrationIds) — sem vínculo, as rotas respondem 403.

Quickstart

  1. No painel Integrações, crie uma integração PUBLIC_API.
  2. Gere e copie o apiToken e o webhookSecret.
  3. Informe o webhookUrl (HTTPS) e marque os eventos desejados.
  4. Selecione os números WhatsApp vinculados — ou deixe vazio para todos.
  5. Para publicar posts, marque as integrações Meta em "Publicações Meta" (e, se quiser, contas específicas).
  6. Use o Bearer token nas chamadas REST (ex.: POST /messages com phone) e verifique o HMAC nos webhooks.

Base URL

https://api.manager.nevoa.co/api/public/v1

Versão atual: v1. Mudanças incompatíveis geram nova versão (v2). A v1 permanece estável. O envelope do webhook inclui apiVersion: "v1".

Autenticação

Todas as rotas em /api/public/v1/** exigem o token da integração PUBLIC_API ACTIVE.

Header

Authorization: Bearer <apiToken>

Exemplo

curl -s \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  "https://api.manager.nevoa.co/api/public/v1/channels"

Rotação de token

  1. Gere um novo token no painel da integração.
  2. Atualize o cliente para o novo token.
  3. Salve a integração — o token antigo deixa de funcionar imediatamente.
  4. O webhookSecret (HMAC) é independente e pode ser rotacionado da mesma forma.
StatusQuando
401Header ausente, formato inválido, token inexistente ou integração INACTIVE
403Rotas /social sem integração Meta vinculada (linkedMetaIntegrationIds) — publicação é opt-in
404Recurso fora do escopo linkedIntegrationIds ou do escopo Meta (não vaza existência)
409Conflito de negócio (ex.: TEMPLATE_REQUIRED ao enviar free-form fora da janela 24h)
429Rate limit da Public API excedido (padrão 120 req/min por integração)

Canais e escopo

config.linkedIntegrationIds lista as integrações META_WHATSAPP vinculadas. Lista vazia ou ausente = todos os números do tenant (inclusive futuros).

Objeto channel

JSON

{
  "integrationId": "wa-uuid",
  "name": "WhatsApp Comercial",
  "provider": "META_WHATSAPP",
  "phoneNumberId": "123",
  "displayPhoneNumber": "+5511999999999"
}
  • Em listagens, use o filtro channelIntegrationId para restringir a um número (deve pertencer ao escopo).
  • Rotas com :id de conversa/contato fora do vínculo retornam 404 (não vazam existência).
  • Todo payload de mensagem/conversa (API e webhook) inclui o objeto channel.

Canais

Ações — clique para expandir

Endpoints REST

Conversas

Ações — clique para expandir

Mensagens / Templates

Envie free-form por telefone/contato (última conversa) ou inicie com template aprovado.

Ações — clique para expandir

Contatos

Ações — clique para expandir

Catálogos

Ações — clique para expandir

Webhook deliveries

Ações — clique para expandir

Publicações (Facebook/Instagram)

Exige integração Meta vinculada em config.linkedMetaIntegrationIds (opt-in): sem vínculo, todas as rotas /social respondem 403. Opcionalmente, config.linkedMetaAssetIds restringe a Páginas/contas Instagram específicas. Ciclo de vida: DRAFT → SCHEDULED → PUBLISHING → PUBLISHED | PARTIALLY_PUBLISHED | FAILED; CANCELED via /cancel. Editáveis: DRAFT, SCHEDULED, CANCELED e FAILED. Para mudar a data de um agendamento: teste com POST /posts/:id/validate, aplique com POST /posts/:id/reschedule (se falhar, o agendamento atual continua valendo) ou envie scheduledAt no PATCH junto com outras edições. Datas em ISO 8601 com fuso (ex.: 2026-10-10T12:00:00-03:00); respostas em UTC. Agendamento exige pelo menos 2 minutos de antecedência.

Ações — clique para expandir

Webhooks

A Névoa entrega eventos no seu webhookUrl com envelope versionado e assinatura HMAC.

Envelope e headers

Envelope

{
  "id": "uuid-da-entrega",
  "event": "message.received",
  "apiVersion": "v1",
  "occurredAt": "2026-07-19T12:00:00.000Z",
  "data": {}
}
  • Content-Type — application/json
  • X-Nevoa-Event — Tipo do evento
  • X-Nevoa-Delivery-Id — ID da entrega (idempotência)
  • X-Nevoa-Timestamp — Unix epoch (segundos)
  • X-Nevoa-Signature — sha256=<hmac-sha256 hex de `${timestamp}.${corpoBruto}`>
  • User-Agent — Nevoa-PublicAPI/1.0

Política de entrega

  • Timeout: 10s
  • Retries: 5 tentativas, backoff exponencial (5s, 10s, 20s, 40s, 80s)
  • Sucesso: HTTP 2xx
  • Idempotência: use X-Nevoa-Delivery-Id / id do envelope
  • Integração INACTIVE ou isWebhookEnabled=false → entrega DISABLED
  • subscribedEvents ausente ou vazio = todos os eventos, inclusive os lançados no futuro
  • HMAC inclui timestamp; rejeite skew > 300s (anti-replay)

Objetos conversation e contact

JSON

{
  "conversation": {
    "id": "conv-uuid",
    "status": "OPEN",
    "sector": { "id": "sector-uuid", "name": "Suporte" },
    "lastMessage": { "direction": "INBOUND", "at": "2026-08-11T12:00:00.000Z" }
  },
  "contact": {
    "id": "contact-uuid",
    "name": "Maria",
    "primaryPhoneE164": "+5511888888888",
    "lastMessageAt": "2026-08-11T12:00:00.000Z"
  }
}
  • Todos os eventos com conversa/contato incluem esses objetos com o estado atual no momento da emissão.
  • conversation.sector: setor atual ({ id, name } ou null).
  • conversation.lastMessage: última mensagem visível ({ direction: INBOUND|OUTBOUND, at } ou null). INBOUND = contato enviou; OUTBOUND = equipe enviou. Notas internas não entram.
  • contact.lastMessageAt: data ISO da última mensagem enviada pelo contato, em qualquer conversa (null se nunca enviou).
  • Os campos conversationId / contactId continuam presentes nos eventos que já os tinham (retrocompatibilidade).

Verificação HMAC

Node.js

const crypto = require('crypto');

function verify(rawBody, secret, signatureHeader, timestampHeader) {
  const ts = Number(timestampHeader);
  if (!Number.isFinite(ts) || Math.abs(Date.now() / 1000 - ts) > 300) {
    return false;
  }
  const expected =
    'sha256=' +
    crypto
      .createHmac('sha256', secret)
      .update(`${ts}.${rawBody}`, 'utf8')
      .digest('hex');
  const a = Buffer.from(expected);
  const b = Buffer.from(signatureHeader || '');
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

Python

import hmac, hashlib, time

def verify(raw_body: bytes, secret: str, signature_header: str, timestamp_header: str) -> bool:
    try:
        ts = int(timestamp_header)
    except (TypeError, ValueError):
        return False
    if abs(int(time.time()) - ts) > 300:
        return False
    payload = f"{ts}.".encode() + raw_body
    digest = hmac.new(secret.encode(), payload, hashlib.sha256).hexdigest()
    expected = f"sha256={digest}"
    return hmac.compare_digest(expected, signature_header or "")

Catálogo de eventos

Eventos — clique para expandir

Erros

400 — Requisição inválida

Body ou query string fora do contrato: campo obrigatório ausente, enum inválido, filtro não suportado (ex.: assigned=me).

Exemplo

{ "statusCode": 400, "message": ["type must be one of the following values: TEXT, IMAGE, VIDEO, AUDIO, DOCUMENT"], "error": "Bad Request" }

401 — Não autorizado

Bearer ausente, inválido ou integração PUBLIC_API inativa.

Exemplo

{ "statusCode": 401, "message": "Token de API inválido ou integração inativa" }

404 — Não encontrado (escopo)

Conversa, contato ou canal fora de linkedIntegrationIds. A API responde 404 para não vazar existência.

Exemplo

{ "statusCode": 404, "message": "Conversa não encontrada" }

409 — TEMPLATE_REQUIRED

Envio free-form fora da janela de 24h do WhatsApp. Use template.

Exemplo

{
  "code": "TEMPLATE_REQUIRED",
  "message": "Janela de 24h indisponível. É necessário aguardar resposta do cliente ou usar template."
}

429 — Rate limit excedido

Limite de 120 requisições por minuto por integração. Aguarde e tente novamente com backoff.

Exemplo

{ "statusCode": 429, "message": "Limite de requisições da Public API excedido. Tente novamente em instantes." }

Changelog

v1 · 2026-10-14

  • POST /social/posts/:id/reschedule — muda só a data de um post agendado; com erro, o agendamento atual continua valendo
  • POST /social/posts/:id/unschedule — tira da programação e volta para DRAFT, mantendo conteúdo, destinos e data
  • POST /social/posts/:id/validate — valida um post salvo, opcionalmente com outra data, sem gravar
  • PATCH /social/posts/:id aceita scheduledAt (reagenda post agendado; nos demais status grava a data prevista e null apaga)
  • PATCH /social/posts/:id/media/order e DELETE /social/posts/:id/media/:mediaId
  • Em post agendado, mudanças de mídia que deixariam a publicação inválida são recusadas com 400 sem gravar
  • GET /social/posts: status aceita vários valores separados por vírgula, novo filtro updatedSince e parâmetro order (asc/desc); posts sem data ficam por último
  • Novo evento social_post.unscheduled; social_post.scheduled também é emitido quando a data de um post agendado muda

v1 · 2026-10-07

  • Publicações Meta: rotas /social para criar, agendar, publicar, cancelar e reenviar posts no Facebook e Instagram
  • Vínculo opt-in em config.linkedMetaIntegrationIds (sem vínculo = 403) e allowlist opcional config.linkedMetaAssetIds
  • Header Idempotency-Key em POST /social/posts
  • Eventos social_post.scheduled, social_post.published, social_post.partially_published, social_post.failed e social_post.canceled

v1 · 2026-08-11

  • Todos os eventos de webhook com conversa/contato incluem os objetos conversation e contact com o estado atual
  • conversation.sector — setor atual da conversa ({ id, name } ou null)
  • conversation.lastMessage — última mensagem visível ({ direction: INBOUND|OUTBOUND, at } ou null); notas internas não contam
  • contact.lastMessageAt — data ISO da última mensagem enviada pelo contato (ou null)
  • Campo lastMessageAt também na API REST (GET /contacts, GET /contacts/:id, GET /contacts/by-phone/:phone)

v1 · 2026-07-19

  • Provider PUBLIC_API com apiToken, webhookSecret, webhookUrl, subscribedEvents, isWebhookEnabled, linkedIntegrationIds
  • Webhook de saída (outbox + HMAC + retries) para 8 tipos de evento
  • Objeto channel em payloads de mensagem/conversa
  • API REST /api/public/v1 (conversas, mensagens, templates, contatos, catálogos, channels, webhook-deliveries)
  • POST /messages — free-form por phone/contactId (conversa mais recente no escopo)