Developers/Webhooks/Documentação Webhook

Documentação Webhook

Receba notificações em tempo real quando eventos ocorrem no Bunto ERP via HTTP POST com assinatura HMAC-SHA256.

13/02/202611 min de leitura8 visualizações

Receba notificações em tempo real quando eventos ocorrem no Bunto ERP. Webhooks enviam requisições HTTP POST automaticamente para a URL configurada, com payload assinado via HMAC-SHA256.

Os webhooks são disparados tanto para operações realizadas via painel do ERP (autenticação JWT) quanto via API externa (token bnt_), garantindo que qualquer alteração no sistema gere a notificação correspondente.

Visão Geral

PropriedadeValor
MétodoPOST (enviado pelo Bunto para sua URL)
FormatoJSON (application/json; charset=utf-8)
AssinaturaHMAC-SHA256 (header X-Bunto-Signature)
RetriesBackoff exponencial (30s, 60s, 120s, 240s...)
TimeoutConfigurável por webhook (5-30 segundos)
Retenção de logs30 dias (LGPD)
Limite20 webhooks por empresa
HTTPSObrigatório em produção

Como funciona: Quando um evento ocorre (ex: produto criado, venda finalizada), o Bunto envia um POST para cada webhook ativo que escuta aquele evento. Sua aplicação recebe o payload, verifica a assinatura e processa os dados.


Configuração

Criando um Webhook

  1. Acesse o painel do Bunto ERP
  2. Navegue até Integrações > Webhooks
  3. Clique em Novo Webhook
  4. Preencha:
    • URL de destino: Endpoint HTTPS que receberá as notificações
    • Descrição: Ex: "Notificações para sistema de logística"
    • Eventos: Selecione quais eventos disparam este webhook
    • Máx. tentativas: 1 a 10 (padrão: 5)
    • Timeout: 5 a 30 segundos (padrão: 10)
  5. Ao salvar, o sistema gera um Secret — copie e armazene no seu servidor

Importante: O secret é necessário para verificar a assinatura das requisições (veja Verificação de Assinatura). Se perdê-lo, use a opção "Rotacionar Secret" para gerar um novo.


Eventos Disponíveis

EventoDescriçãoQuando é disparado
produto.criadoProduto criadoNovo produto cadastrado no sistema
produto.atualizadoProduto atualizadoDados do produto alterados (nome, preço, situação, etc.)
produto.excluidoProduto excluídoProduto removido do sistema
cliente.criadoCliente criadoNovo cliente cadastrado
cliente.atualizadoCliente atualizadoDados do cliente alterados
cliente.excluidoCliente excluídoCliente removido do sistema
venda.criadaVenda criadaNova venda/pedido registrado
venda.atualizadaVenda atualizadaDados da venda alterados (status, observação, etc.)
venda.canceladaVenda canceladaVenda cancelada no sistema
estoque.atualizadoEstoque atualizadoLançamento de entrada, saída ou balanço de estoque
nfe.emitidaNF-e emitidaNota fiscal eletrônica autorizada pela SEFAZ
nfe.canceladaNF-e canceladaNota fiscal eletrônica cancelada

Além desses, existe o evento especial webhook.teste usado pelo botão "Testar" na interface.


Estrutura do Payload

Todos os eventos seguem a mesma estrutura base:

json
{ "evento": "produto.criado", "timestamp": "2026-02-13T13:56:47.865088+00:00", "webhook_id": 3, "empresa_id": 1, "dados": { ... }, "idempotency_key": "3_produto.criado_38951ae5a9d440a1" }
CampoTipoDescrição
eventostringTipo do evento (ex: produto.criado)
timestampstring (ISO 8601)Data/hora do evento com timezone
webhook_idintegerID do webhook que recebeu o evento
empresa_idintegerID da empresa no Bunto ERP
dadosobjectDados específicos do evento (varia por tipo)
idempotency_keystringChave única para evitar processamento duplicado

Exemplos de Payload por Evento

produto.criado

Disparado quando um novo produto é cadastrado, seja pelo painel do ERP ou pela API.

json
{ "evento": "produto.criado", "timestamp": "2026-02-13T13:56:47.865088+00:00", "webhook_id": 3, "empresa_id": 1, "dados": { "id": 640, "nome": "Camiseta Básica Algodão - M Preto", "codigo": "CAM001-M-PT", "preco_venda": "49.90", "preco_custo": "22.50", "unidade": "UN", "situacao": "A", "ativo": true, "gtin": "7891234567890", "criado_em": "2026-02-13 13:56:47.845604+00:00" }, "idempotency_key": "3_produto.criado_38951ae5a9d440a1" }
CampoTipoDescrição
idintegerID do produto
nomestringNome do produto
codigostringCódigo/SKU do produto
preco_vendastringPreço de venda (decimal como string)
preco_custostringPreço de custo (decimal como string)
unidadestringUnidade de medida (UN, KG, L, etc.)
situacaostringSituação: A (Ativo), I (Inativo), E (Excluído)
ativobooleanSe o produto está ativo
gtinstring/nullCódigo GTIN/EAN
criado_emstringData de criação

produto.atualizado

Disparado quando qualquer campo do produto é alterado.

json
{ "evento": "produto.atualizado", "timestamp": "2026-02-13T13:56:49.194533+00:00", "webhook_id": 3, "empresa_id": 1, "dados": { "id": 640, "nome": "Camiseta Básica Algodão - M Preto", "codigo": "CAM001-M-PT", "preco_venda": "59.90", "preco_custo": "22.50", "unidade": "UN", "situacao": "A", "ativo": true, "gtin": "7891234567890", "atualizado_em": "2026-02-13 13:56:49.185592+00:00" }, "idempotency_key": "3_produto.atualizado_7583cb094a794ae2" }

Nota: Quando um produto é excluído via soft-delete, o evento produto.atualizado é disparado com situacao: "E" e ativo: false.

produto.excluido

Disparado quando um produto é removido permanentemente do sistema.

json
{ "evento": "produto.excluido", "timestamp": "2026-02-13T13:56:50.545291+00:00", "webhook_id": 3, "empresa_id": 1, "dados": { "id": 640, "nome": "Camiseta Básica Algodão - M Preto", "codigo": "CAM001-M-PT" }, "idempotency_key": "3_produto.excluido_27892a96252748ac" }

cliente.criado

Disparado quando um novo cliente é cadastrado.

LGPD: Dados sensíveis (CPF/CNPJ, e-mail, telefone) são mascarados automaticamente no payload do webhook para conformidade com a Lei Geral de Proteção de Dados.

json
{ "evento": "cliente.criado", "timestamp": "2026-02-13T13:56:09.453507+00:00", "webhook_id": 3, "empresa_id": 1, "dados": { "id": 364, "nome": "Maria Oliveira Santos", "tipo_pessoa": 2, "cnpj_cpf": "**.456.789/0001-00", "email": "m***@empresa.com.br", "celular": "*****-5678", "cidade": "Rio de Janeiro", "uf": "RJ" }, "idempotency_key": "3_cliente.criado_317206bacfaa408d" }
CampoTipoDescrição
idintegerID do cliente
nomestringNome/Razão social
tipo_pessoainteger1 = Pessoa Física, 2 = Pessoa Jurídica
cnpj_cpfstringCPF/CNPJ mascarado (LGPD)
emailstringE-mail mascarado (LGPD)
celularstringTelefone mascarado (LGPD)
cidadestring/nullCidade
ufstring/nullEstado (UF)

cliente.atualizado

Disparado quando qualquer campo do cliente é alterado.

json
{ "evento": "cliente.atualizado", "timestamp": "2026-02-13T13:56:11.884078+00:00", "webhook_id": 3, "empresa_id": 1, "dados": { "id": 364, "nome": "Maria Oliveira Santos LTDA", "tipo_pessoa": 2, "cnpj_cpf": "**.456.789/0001-00", "email": "c***@empresa.com.br", "celular": "*****-5678", "cidade": "Rio de Janeiro", "uf": "RJ" }, "idempotency_key": "3_cliente.atualizado_18aad6d02a1f46ad" }

cliente.excluido

Disparado quando um cliente é removido do sistema.

json
{ "evento": "cliente.excluido", "timestamp": "2026-02-13T13:56:14.219727+00:00", "webhook_id": 3, "empresa_id": 1, "dados": { "id": 364, "nome": "Maria Oliveira Santos LTDA", "cnpj_cpf": "**.456.789/0001-00" }, "idempotency_key": "3_cliente.excluido_4e06734635234632" }

venda.criada

Disparado quando uma nova venda/pedido é registrado.

json
{ "evento": "venda.criada", "timestamp": "2026-02-13T14:10:22.331456+00:00", "webhook_id": 3, "empresa_id": 1, "dados": { "id": 892, "numero_pedido": "001245", "status": "em_aberto", "tipo_pedido": "venda", "total_venda": "299.70", "cliente_id": 364 }, "idempotency_key": "3_venda.criada_c3d4e5f6g7h8i9j0" }
CampoTipoDescrição
idintegerID da venda
numero_pedidostringNúmero do pedido
statusstringStatus atual da venda
tipo_pedidostringTipo: venda, orcamento, pedido
total_vendastringValor total (decimal como string)
cliente_idintegerID do cliente

venda.atualizada

Disparado quando qualquer campo da venda é alterado.

json
{ "evento": "venda.atualizada", "timestamp": "2026-02-13T14:15:33.456789+00:00", "webhook_id": 3, "empresa_id": 1, "dados": { "id": 892, "numero_pedido": "001245", "status": "finalizada", "tipo_pedido": "venda", "total_venda": "299.70", "cliente_id": 364, "status_anterior": "em_aberto" }, "idempotency_key": "3_venda.atualizada_d4e5f6g7h8i9j0k1" }

venda.cancelada

Disparado quando o status da venda muda para cancelada.

json
{ "evento": "venda.cancelada", "timestamp": "2026-02-13T14:20:44.567890+00:00", "webhook_id": 3, "empresa_id": 1, "dados": { "id": 892, "numero_pedido": "001245", "status": "cancelada", "tipo_pedido": "venda", "total_venda": "299.70", "cliente_id": 364, "status_anterior": "finalizada" }, "idempotency_key": "3_venda.cancelada_e5f6g7h8i9j0k1l2" }

estoque.atualizado

Disparado quando há qualquer movimentação de estoque (entrada, saída ou balanço).

json
{ "evento": "estoque.atualizado", "timestamp": "2026-02-13T13:56:55.721908+00:00", "webhook_id": 3, "empresa_id": 1, "dados": { "id": 2223, "produto_id": 266, "deposito_id": 4, "tipo": "E", "origem": "M", "quantidade": "5.000", "saldo_atual": "70.000", "data_lancamento": "2026-02-13 13:56:55.695372+00:00", "produto_nome": "Camiseta Básica Algodão - G Branco", "produto_codigo": "CAM001-G-BR", "deposito_nome": "Próspera" }, "idempotency_key": "3_estoque.atualizado_822c699af3b74d26" }
CampoTipoDescrição
idintegerID do lançamento
produto_idintegerID do produto
deposito_idintegerID do depósito
tipostringE (Entrada), S (Saída), B (Balanço)
origemstringM (Manual), V (Venda), C (Compra), D (Devolução), N (Nota Fiscal)
quantidadestringQuantidade movimentada (decimal como string)
saldo_atualstringSaldo após o lançamento (decimal como string)
data_lancamentostringData/hora do lançamento
produto_nomestringNome do produto
produto_codigostringCódigo/SKU do produto
deposito_nomestringNome do depósito

nfe.emitida

Disparado quando uma NF-e é autorizada pela SEFAZ.

json
{ "evento": "nfe.emitida", "timestamp": "2026-02-13T15:30:45.123456+00:00", "webhook_id": 3, "empresa_id": 1, "dados": { "id": 5001, "tipo": "NFE", "numero": "12345", "serie": "1", "chave_acesso": "35260213999999000199550010000123451000000001", "status": "AUTORIZADO", "valor_total": "299.70", "protocolo": "135260000012345", "cliente_id": 364, "criado_em": "2026-02-13 15:30:00.000000+00:00" }, "idempotency_key": "3_nfe.emitida_e5f6g7h8i9j0k1l2" }
CampoTipoDescrição
idintegerID do documento fiscal
tipostringNFE (Nota Fiscal Eletrônica) ou NFCE (Consumidor)
numerostringNúmero da nota
seriestringSérie da nota
chave_acessostringChave de acesso (44 dígitos)
statusstringAUTORIZADO
valor_totalstringValor total da nota (decimal como string)
protocolostringProtocolo de autorização da SEFAZ
cliente_idintegerID do cliente/destinatário
criado_emstringData de criação do documento

nfe.cancelada

Disparado quando uma NF-e é cancelada.

json
{ "evento": "nfe.cancelada", "timestamp": "2026-02-13T16:00:12.654321+00:00", "webhook_id": 3, "empresa_id": 1, "dados": { "id": 5001, "tipo": "NFE", "numero": "12345", "serie": "1", "chave_acesso": "35260213999999000199550010000123451000000001", "status": "CANCELADO", "valor_total": "299.70", "protocolo_cancelamento": "135260000099999" }, "idempotency_key": "3_nfe.cancelada_f6g7h8i9j0k1l2m3" }

webhook.teste

Evento especial disparado pelo botão "Testar" na interface ou pela API de teste.

json
{ "evento": "webhook.teste", "timestamp": "2026-02-13T13:50:00.000000+00:00", "webhook_id": 3, "empresa_id": 1, "dados": { "mensagem": "Este é um evento de teste. Se você recebeu, seu webhook está funcionando!" }, "idempotency_key": "3_teste_a1b2c3d4e5f6g7h8" }

Headers da Requisição

Cada requisição de webhook inclui os seguintes headers:

HeaderExemploDescrição
Content-Typeapplication/json; charset=utf-8Formato do payload
User-AgentBuntoERP-Webhook/1.0Identificação do sistema
X-Bunto-Signaturesha256=63fad96bb1c44e80...Assinatura HMAC-SHA256 do payload
X-Bunto-Eventoestoque.atualizadoTipo do evento
X-Bunto-Webhook-Id3ID do webhook
X-Bunto-Timestamp2026-02-13T13:56:55.721908+00:00Timestamp do evento (ISO 8601 com timezone)
X-Bunto-Idempotency-Key3_estoque.atualizado_822c699af3b74d26Chave de idempotência

Exemplo real de headers recebidos:

code
user-agent: BuntoERP-Webhook/1.0 content-type: application/json; charset=utf-8 x-bunto-signature: sha256=63fad96bb1c44e8027877ee8d8c99c68b8fe2a2de3f655888a0ead907ea783b6 x-bunto-evento: estoque.atualizado x-bunto-webhook-id: 3 x-bunto-timestamp: 2026-02-13T13:56:55.721908+00:00 x-bunto-idempotency-key: 3_estoque.atualizado_822c699af3b74d26

Além desses, headers customizados configurados no webhook também são incluídos.


Verificação de Assinatura (Segurança)

Sempre verifique a assinatura para garantir que a requisição veio do Bunto ERP e não foi alterada.

Como funciona

  1. O Bunto calcula HMAC-SHA256(secret, payload_json) usando o secret do webhook
  2. O resultado é enviado no header X-Bunto-Signature com prefixo sha256=
  3. Sua aplicação recalcula o HMAC com o mesmo secret e compara com o header
  4. Use comparação constant-time para evitar timing attacks

Python

python
import hmac import hashlib import json from flask import Flask, request, abort app = Flask(__name__) WEBHOOK_SECRET = "whsec_seu_secret_aqui" def verificar_assinatura(payload_body: bytes, assinatura_header: str) -> bool: """Verifica assinatura HMAC-SHA256 do webhook Bunto.""" mac = hmac.new( WEBHOOK_SECRET.encode('utf-8'), payload_body, hashlib.sha256 ) assinatura_esperada = f"sha256={mac.hexdigest()}" return hmac.compare_digest(assinatura_esperada, assinatura_header) @app.route('/webhook', methods=['POST']) def receber_webhook(): assinatura = request.headers.get('X-Bunto-Signature', '') if not verificar_assinatura(request.data, assinatura): abort(401, 'Assinatura inválida') payload = request.json evento = payload['evento'] dados = payload['dados'] if evento == 'produto.criado': print(f"Novo produto: {dados['nome']} (ID: {dados['id']})") elif evento == 'estoque.atualizado': print(f"Estoque: {dados['produto_nome']} | {dados['tipo']} {dados['quantidade']}") elif evento == 'cliente.criado': print(f"Novo cliente: {dados['nome']} ({dados['cidade']}/{dados['uf']})") return {'status': 'ok'}, 200

JavaScript (Node.js)

javascript
const crypto = require('crypto'); const express = require('express'); const app = express(); const WEBHOOK_SECRET = 'whsec_seu_secret_aqui'; app.use(express.raw({ type: 'application/json' })); function verificarAssinatura(payloadBody, assinaturaHeader) { const mac = crypto.createHmac('sha256', WEBHOOK_SECRET) .update(payloadBody) .digest('hex'); const assinaturaEsperada = `sha256=${mac}`; return crypto.timingSafeEqual( Buffer.from(assinaturaEsperada), Buffer.from(assinaturaHeader) ); } app.post('/webhook', (req, res) => { const assinatura = req.headers['x-bunto-signature'] || ''; if (!verificarAssinatura(req.body, assinatura)) { return res.status(401).json({ erro: 'Assinatura inválida' }); } const payload = JSON.parse(req.body); const { evento, dados } = payload; switch (evento) { case 'produto.criado': console.log(`Novo produto: ${dados.nome} (ID: ${dados.id})`); break; case 'estoque.atualizado': console.log(`Estoque: ${dados.produto_nome} | ${dados.tipo} ${dados.quantidade}`); break; case 'cliente.criado': console.log(`Novo cliente: ${dados.nome}`); break; } res.status(200).json({ status: 'ok' }); }); app.listen(3000);

PHP

php
<?php $secret = 'whsec_seu_secret_aqui'; $payload = file_get_contents('php://input'); $assinatura = $_SERVER['HTTP_X_BUNTO_SIGNATURE'] ?? ''; $mac = 'sha256=' . hash_hmac('sha256', $payload, $secret); if (!hash_equals($mac, $assinatura)) { http_response_code(401); echo json_encode(['erro' => 'Assinatura inválida']); exit; } $dados = json_decode($payload, true); $evento = $dados['evento']; switch ($evento) { case 'produto.criado': $produto = $dados['dados']; error_log("Novo produto: {$produto['nome']} (ID: {$produto['id']})"); break; case 'estoque.atualizado': $estoque = $dados['dados']; error_log("Estoque: {$estoque['produto_nome']} | {$estoque['tipo']} {$estoque['quantidade']}"); break; case 'cliente.criado': $cliente = $dados['dados']; error_log("Novo cliente: {$cliente['nome']}"); break; } http_response_code(200); echo json_encode(['status' => 'ok']);

Java (Spring Boot)

java
import javax.crypto.Mac; import javax.crypto.spec.SecretKeySpec; import org.springframework.web.bind.annotation.*; import org.springframework.http.ResponseEntity; import java.util.Map; @RestController public class WebhookController { private static final String WEBHOOK_SECRET = "whsec_seu_secret_aqui"; private boolean verificarAssinatura(String payload, String assinaturaHeader) { try { Mac mac = Mac.getInstance("HmacSHA256"); mac.init(new SecretKeySpec(WEBHOOK_SECRET.getBytes("UTF-8"), "HmacSHA256")); byte[] hash = mac.doFinal(payload.getBytes("UTF-8")); StringBuilder hex = new StringBuilder(); for (byte b : hash) { hex.append(String.format("%02x", b)); } String assinaturaEsperada = "sha256=" + hex.toString(); return assinaturaEsperada.equals(assinaturaHeader); } catch (Exception e) { return false; } } @PostMapping("/webhook") public ResponseEntity<Map<String, String>> receberWebhook( @RequestBody String payload, @RequestHeader("X-Bunto-Signature") String assinatura, @RequestHeader("X-Bunto-Evento") String evento) { if (!verificarAssinatura(payload, assinatura)) { return ResponseEntity.status(401).body(Map.of("erro", "Assinatura inválida")); } switch (evento) { case "produto.criado": System.out.println("Novo produto recebido via webhook"); break; case "estoque.atualizado": System.out.println("Estoque atualizado via webhook"); break; case "cliente.criado": System.out.println("Novo cliente recebido via webhook"); break; } return ResponseEntity.ok(Map.of("status", "ok")); } }

C# (.NET)

csharp
using System.Security.Cryptography; using System.Text; using Microsoft.AspNetCore.Mvc; [ApiController] [Route("webhook")] public class WebhookController : ControllerBase { private const string WebhookSecret = "whsec_seu_secret_aqui"; private bool VerificarAssinatura(string payload, string assinaturaHeader) { using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(WebhookSecret)); var hash = hmac.ComputeHash(Encoding.UTF8.GetBytes(payload)); var assinaturaEsperada = "sha256=" + BitConverter.ToString(hash) .Replace("-", "").ToLower(); return assinaturaEsperada == assinaturaHeader; } [HttpPost] public IActionResult ReceberWebhook( [FromBody] dynamic payload, [FromHeader(Name = "X-Bunto-Signature")] string assinatura, [FromHeader(Name = "X-Bunto-Evento")] string evento) { string body; using (var reader = new StreamReader(Request.Body)) { body = reader.ReadToEnd(); } if (!VerificarAssinatura(body, assinatura)) { return Unauthorized(new { erro = "Assinatura inválida" }); } switch (evento) { case "produto.criado": Console.WriteLine("Novo produto recebido via webhook"); break; case "estoque.atualizado": Console.WriteLine("Estoque atualizado via webhook"); break; case "cliente.criado": Console.WriteLine("Novo cliente recebido via webhook"); break; } return Ok(new { status = "ok" }); } }

Go

go
package main import ( "crypto/hmac" "crypto/sha256" "encoding/hex" "encoding/json" "fmt" "io" "net/http" ) const webhookSecret = "whsec_seu_secret_aqui" func verificarAssinatura(payload []byte, assinaturaHeader string) bool { mac := hmac.New(sha256.New, []byte(webhookSecret)) mac.Write(payload) assinaturaEsperada := "sha256=" + hex.EncodeToString(mac.Sum(nil)) return hmac.Equal([]byte(assinaturaEsperada), []byte(assinaturaHeader)) } func webhookHandler(w http.ResponseWriter, r *http.Request) { body, err := io.ReadAll(r.Body) if err != nil { http.Error(w, "Erro ao ler body", 400) return } assinatura := r.Header.Get("X-Bunto-Signature") if !verificarAssinatura(body, assinatura) { http.Error(w, `{"erro":"Assinatura inválida"}`, 401) return } var payload map[string]interface{} json.Unmarshal(body, &payload) evento := payload["evento"].(string) dados := payload["dados"].(map[string]interface{}) switch evento { case "produto.criado": fmt.Printf("Novo produto: %s (ID: %.0f)\n", dados["nome"], dados["id"]) case "estoque.atualizado": fmt.Printf("Estoque: %s | %s %s\n", dados["produto_nome"], dados["tipo"], dados["quantidade"]) case "cliente.criado": fmt.Printf("Novo cliente: %s\n", dados["nome"]) } w.Header().Set("Content-Type", "application/json") w.WriteHeader(200) w.Write([]byte(`{"status":"ok"}`)) } func main() { http.HandleFunc("/webhook", webhookHandler) fmt.Println("Servidor webhook rodando na porta 3000") http.ListenAndServe(":3000", nil) }

Política de Retries

Quando o webhook falha (timeout, erro de conexão ou resposta não-2xx), o Bunto re-tenta com backoff exponencial:

TentativaIntervaloTempo acumulado
1Imediata0s
230 segundos30s
31 minuto1m 30s
42 minutos3m 30s
54 minutos7m 30s
68 minutos15m 30s
716 minutos31m 30s
832 minutos63m 30s
964 minutos~2h 7m
10128 minutos~4h 15m
  • O número máximo de tentativas é configurável (1-10, padrão: 5)
  • Após esgotar todas as tentativas, o evento é marcado como falha definitiva
  • Cada tentativa gera um registro de log independente

O que conta como sucesso

Código HTTPResultado
2xx (200-299)Sucesso - não faz retry
3xx (redirect)Falha - redirects bloqueados por segurança
4xx (400-499)Falha - faz retry (pode ser erro temporário no receptor)
5xx (500-599)Falha - faz retry
TimeoutFalha - faz retry
Erro de conexãoFalha - faz retry

Idempotência

Cada entrega possui uma idempotency_key única, enviada no header X-Bunto-Idempotency-Key e no corpo do payload.

Use essa chave para evitar processamento duplicado. Mesmo com as proteções internas do Bunto, é possível que sua aplicação receba o mesmo evento mais de uma vez (ex: se sua aplicação respondeu 200 mas houve timeout no retorno).

Formato: {webhook_id}_{evento}_{hash_unico}

Exemplo: 3_estoque.atualizado_822c699af3b74d26

python
# Exemplo: armazenar chaves processadas chaves_processadas = set() # Use Redis ou banco de dados em produção @app.route('/webhook', methods=['POST']) def receber_webhook(): payload = request.json chave = payload['idempotency_key'] if chave in chaves_processadas: return {'status': 'já processado'}, 200 # Processar evento... chaves_processadas.add(chave) return {'status': 'ok'}, 200

LGPD - Proteção de Dados

Em conformidade com a Lei Geral de Proteção de Dados (Lei 13.709/2018), os webhooks do Bunto mascaram automaticamente dados pessoais sensíveis nos payloads:

DadoFormato mascaradoExemplo
CPF***.XXX.XXX-XX***.456.789-00
CNPJ**.XXX.XXX/XXXX-XX**.456.789/0001-00
E-mailX***@dominio.comm***@empresa.com.br
Telefone*****-XXXX*****-5678

Nota: Para acessar dados completos do cliente, utilize a API de Clientes (GET /v1/clientes/{id}/) com autenticação via token bnt_.


Segurança

Validação de URL (SSRF)

O Bunto valida a URL de destino contra ataques SSRF:

  • Bloqueados: IPs privados (10.x, 172.16-31.x, 192.168.x), localhost, metadata endpoints (169.254.169.254)
  • Bloqueados: URLs com credenciais embutidas (user:pass@host)
  • Obrigatório: HTTPS em produção (HTTP permitido apenas em desenvolvimento)
  • Bloqueados: Redirects (requisições nunca seguem 301/302)

Secret

  • Gerado automaticamente na criação do webhook
  • Exibido apenas na criação e na rotação — armazene com segurança
  • Pode ser rotacionado a qualquer momento (invalida o anterior imediatamente)

Boas Práticas

  1. Sempre verifique a assinatura antes de processar o payload
  2. Use HTTPS no seu endpoint (obrigatório em produção)
  3. Responda rápido (dentro do timeout configurado) - processe assincronamente
  4. Implemente idempotência usando a idempotency_key
  5. Rotacione o secret periodicamente ou se houver suspeita de comprometimento
  6. Monitore os logs de entrega na interface do Bunto

Limites e Quotas

RecursoLimite
Webhooks por empresa20
Headers customizados por webhook10
Tamanho da chave de header128 caracteres
Tamanho do valor de header1.024 caracteres
Tamanho da URL2.048 caracteres
Tamanho da descrição255 caracteres
Response body armazenado2 KB (truncado)
Retenção de logs30 dias
Máx. tentativas10
Timeout5-30 segundos

Troubleshooting

Webhook não está recebendo eventos

  1. Verifique se o webhook está ativo (campo ativo: true)
  2. Confirme que os eventos corretos estão selecionados
  3. Use o botão Testar para verificar conectividade
  4. Verifique os logs de entrega para identificar erros

Assinatura inválida

  1. Confirme que está usando o secret correto (não confunda com o prefixo)
  2. Verifique que está calculando o HMAC sobre o body raw (não o JSON parseado)
  3. Use comparação constant-time (hmac.compare_digest em Python, crypto.timingSafeEqual em Node.js)

Timeout frequente

  1. Aumente o timeout do webhook (até 30s)
  2. Processe assincronamente: responda 200 imediatamente e processe em background
  3. Verifique a performance do seu servidor

Muitas falhas

  1. Verifique se seu servidor está acessível externamente
  2. Confirme que está respondendo com código 2xx (200-299)
  3. Não retorne redirects (3xx) - são bloqueados por segurança

FAQ

P: Os webhooks disparam para operações via API e via painel do ERP? R: Sim. Qualquer alteração no sistema dispara o webhook, independente de ter sido feita pelo painel (JWT) ou pela API externa (token bnt_).

P: Posso usar HTTP em vez de HTTPS? R: Apenas em desenvolvimento. Em produção, HTTPS é obrigatório.

P: O que acontece se meu servidor estiver fora do ar? R: O Bunto re-tenta com backoff exponencial até o máximo de tentativas configurado. Após esgotar, o evento é registrado como falha definitiva.

P: Posso receber o mesmo evento mais de uma vez? R: Sim, em casos raros. Use a idempotency_key para evitar processamento duplicado.

P: Como sei que a requisição veio do Bunto? R: Verifique a assinatura HMAC-SHA256 no header X-Bunto-Signature usando seu secret.

P: Perdi o secret, como recuperar? R: O secret não pode ser recuperado. Use a opção "Rotacionar Secret" no painel para gerar um novo.

P: Por que os dados de clientes aparecem mascarados? R: Para conformidade com a LGPD. CPF/CNPJ, e-mail e telefone são mascarados automaticamente. Use a API de Clientes para obter dados completos.

P: Headers customizados podem sobrescrever headers do sistema? R: Não. Headers reservados (Content-Type, Authorization, X-Bunto-*, Cookie, etc.) são protegidos.

APIEventosHMACIntegraçãoLGPDNotificaçõesRESTSegurançaWebhook
Recursos para IA