Webhooks
Webhooks entregam eventos em tempo real do Canal de Denúncias e da Ouvidoria para um endpoint HTTPS do seu sistema, eliminando a necessidade de polling.
:::info Recurso do painel, não da API de integração
Webhooks são configurados dentro do painel Nearone (Configurações do
canal → Webhooks) por um administrador da organização, autenticado por sessão
(JWT) — não por chave de API. Não existe um endpoint POST /webhooks na
superfície /api/integrations/v1 descrita no restante deste site. Esta
página documenta o payload e o contrato de entrega das notificações, que
são o que importa para quem constrói o lado receptor da integração.
:::
Como habilitar
- No painel, acesse Configurações do canal → Webhooks (dentro do Canal de Denúncias ou da Ouvidoria, conforme o módulo).
- Informe a URL HTTPS do seu endpoint e selecione os eventos de interesse.
- Ao salvar, a Nearone gera um segredo — exibido uma única vez — usado para assinar cada entrega.
Catálogo de eventos
Canal de Denúncias (complaint.*)
| Evento | Quando dispara |
|---|---|
complaint.received | Nova denúncia registrada (pública ou por link de acesso) |
complaint.status_changed | Status do caso muda (recebida → em análise → resolvida/arquivada, incl. reabertura) |
complaint.assignee_changed | Responsável pelo caso é atribuído ou removido |
complaint.message_received | Denunciante envia nova mensagem na thread anônima do protocolo |
complaint.investigation_concluded | Conclusão da apuração é aprovada (fluxo de dupla checagem — 4-eyes) |
complaint.legal_hold_set | Retenção legal (legal hold) é ativada no caso |
complaint.legal_hold_released | Retenção legal é liberada |
complaint.sla_breached | Um dos prazos de SLA do caso estoura (acolhimento, retorno ou resolução) |
Ouvidoria (manifestation.*)
| Evento | Quando dispara |
|---|---|
manifestation.received | Nova manifestação registrada |
manifestation.status_changed | Status muda (recebida → acolhida → em tratamento → respondida/encerrada) |
manifestation.assignee_changed | Responsável é atribuído ou removido |
manifestation.message_received | Manifestante envia nova mensagem na thread do protocolo |
manifestation.formal_response_registered | Resposta formal é registrada, encerrando o ciclo de tratamento |
manifestation.reclassified | Manifestação é reclassificada para o Canal de Ética (denúncia grave) |
manifestation.sla_breached | Um dos prazos de SLA estoura (acolhimento ou resposta) |
Formato da entrega
Toda entrega é um POST HTTPS com o corpo:
{
"id": "b3bd5ccd-b182-4608-ab36-a404744ab135",
"type": "complaint.received",
"created_at": "2026-08-01T19:04:55.178994+00:00",
"data": { "...": "campos específicos do evento, ver abaixo" }
}
| Campo | Descrição |
|---|---|
id | Identificador único do evento. Estável entre retentativas da mesma tentativa de entrega — use-o para processamento idempotente do seu lado, já que uma entrega pode ser reenviada. |
type | Um dos eventos do catálogo acima. |
created_at | Timestamp ISO 8601 (UTC) de quando o evento ocorreu. |
data | Corpo específico do evento — ver exemplos abaixo. |
:::info data nunca contém identidade do denunciante/manifestante
O payload traz apenas metadados do caso — código do protocolo, categoria,
status, severidade/prioridade, timestamps. Ele nunca inclui dados
pessoais de quem denunciou ou manifestou (nome, e-mail, conteúdo textual
identificável). O anonimato/confidencialidade do Canal de Denúncias e da
Ouvidoria é um invariante do produto — o payload do webhook respeita a mesma
barreira de identidade que o restante da plataforma.
:::
Exemplos de data por evento:
{
"protocol_code": "DEN-2026-00042",
"category": "assedio_moral",
"severity": "high",
"status": "under_review",
"submitted_at": "2026-08-01T12:00:00Z"
}
{
"protocol_code": "DEN-2026-00042",
"category": "assedio_moral",
"severity": "high",
"status": "under_review",
"previous_status": "received",
"note": "Investigação iniciada."
}
{
"protocol_code": "OUV-2026-00042",
"deadline_type": "response",
"due_at": "2026-08-01T12:00:00Z"
}
Cabeçalhos HTTP
Toda entrega inclui:
| Header | Descrição |
|---|---|
Nearone-Event-Id | Mesmo valor do campo id do corpo. |
Nearone-Event-Type | Mesmo valor do campo type do corpo. |
Nearone-Timestamp | Unix timestamp (segundos) do momento da assinatura. |
Nearone-Signature | Assinatura HMAC-SHA256, ver abaixo. |
User-Agent | NearOne-Webhooks/1.0. |
Content-Type | application/json. |
Verificação de assinatura
A assinatura segue o esquema popularizado pela Stripe:
signature = HMAC-SHA256(secret, f"{timestamp}.{raw_request_body}")
Codificada em hexadecimal e comparada em tempo constante. timestamp é
o valor exato do header Nearone-Timestamp; raw_request_body é o corpo
bruto da requisição, byte a byte.
import hashlib
import hmac
def verify_signature(secret: str, timestamp: str, body: bytes, signature: str) -> bool:
signed_payload = f"{timestamp}.{body.decode()}".encode()
expected = hmac.new(secret.encode(), signed_payload, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, signature)
const crypto = require("crypto");
function verifySignature(secret, timestamp, rawBody, signature) {
const signedPayload = `${timestamp}.${rawBody}`;
const expected = crypto.createHmac("sha256", secret).update(signedPayload).digest("hex");
return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature));
}
:::caution Use sempre o corpo bruto da requisição
Assine/verifique contra os bytes brutos recebidos, nunca contra uma
versão do JSON parseada e reserializada — reserializar pode produzir uma
sequência de bytes diferente (ordem de chaves, espaçamento, escaping) e
quebrar a verificação. Na maioria dos frameworks web isso significa ler o
corpo antes de qualquer parsing automático (express.raw(),
request.get_data() no Flask, etc.) e comparar apenas com
hmac.compare_digest / crypto.timingSafeEqual — nunca ==/===, que são
vulneráveis a ataques de tempo.
:::
O segredo do webhook
O segredo é exibido uma única vez — no momento da criação do webhook, e novamente se você rotacionar. Se for perdido, a única recuperação é rotacionar para um novo segredo, o que invalida o anterior imediatamente. Recomendamos armazená-lo em um secrets manager, nunca em controle de versão.
Política de retentativas
Entregas com falha (resposta não-2xx ou timeout) são reenviadas automaticamente com backoff exponencial, num total de 6 tentativas:
| Tentativa | Atraso desde a anterior |
|---|---|
| 1ª retentativa | 30s |
| 2ª retentativa | 2min |
| 3ª retentativa | 10min |
| 4ª retentativa | 30min |
| 5ª retentativa | 1h |
| 6ª retentativa (final) | 1h |
Depois da 6ª tentativa sem sucesso, a entrega é marcada como failed
(dead-lettered) permanentemente — ela não será reenviada automaticamente
de novo. No log de entregas do painel, um humano pode disparar manualmente um
"reenviar" pontual, que zera o contador de tentativas.
Circuit breaker
Se um endpoint acumular 20 entregas consecutivas totalmente esgotadas (dead-lettered), a Nearone pausa automaticamente o webhook (marca como inativo) em vez de continuar tentando entregar num endpoint claramente quebrado. As entregas pendentes são preservadas — não são perdidas — e voltam a ser processadas assim que o webhook é reativado manualmente no painel, que mostra um indicador de "desativado automaticamente" quando isso acontece.
Boas práticas
- Responda
2xxrápido (na casa de poucos segundos) e faça o trabalho pesado de forma assíncrona depois — uma resposta lenta parece uma falha e dispara uma retentativa desnecessária, mesmo que sua aplicação fosse eventualmente ter sucesso. - Verifique a assinatura em toda requisição antes de confiar no payload — nunca processe um corpo de webhook não verificado.
- Use
Nearone-Event-Idpara idempotência: a entrega é at-least-once, não exactly-once — o mesmo evento pode chegar mais de uma vez se uma retentativa e a tentativa original (atrasada) chegarem ambas ao seu endpoint. - Trate
datacomo metadado, não como o registro completo do caso. Se sua integração precisa do detalhe completo, use o evento como gatilho para voltar ao painel Nearone e consultar a fonte oficial — hoje não existe um endpoint autenticado por chave de API para obter um caso completo por protocolo (só a UI do painel). Na prática, o uso viável agora é automação orientada a metadado ("postar um alerta no Slack quandoseverity=high", "abrir um chamado no seu ITSM"), não sincronização integral de dados. - Use apenas HTTPS. URLs
http://são rejeitadas no cadastro do webhook. Mantenha o certificado TLS do endpoint válido — certificados expirados aparecem como falhas de entrega.
Testando
O painel tem um botão "Testar" por webhook, que envia de forma síncrona
um payload de amostra para qualquer um dos eventos assinados e mostra o
resultado HTTP real inline (status, duração, corpo da resposta) — útil para
validar seu código de verificação de assinatura contra uma requisição
realmente assinada, sem esperar um evento real acontecer. Entregas de teste
aparecem no log de entregas marcadas com is_test: true.