Receber e verificar
Anatomia da requisição, verificação da assinatura HMAC e como o seu endpoint deve responder.
Cada entrega chega como um POST application/json assinado. Antes de processar qualquer coisa, verifique a assinatura — sem isso, qualquer pessoa que descubra a sua URL pode enviar eventos falsos.
Anatomia da requisição
| Header | Exemplo | Para que serve |
|---|---|---|
content-type | application/json | O corpo é sempre JSON |
user-agent | bitERP-Webhooks/1.0 | Identifica a origem (não use como autenticação) |
x-biterp-webhook-id | 018f...:9c2b... | Identificador único do evento — a chave da sua idempotência |
x-biterp-webhook-timestamp | 1774612345 | Momento da assinatura, em segundos desde a época Unix |
x-biterp-signature | sha256=9a1f... | HMAC-SHA256 em hexadecimal, prefixado por sha256= |
O corpo identifica o que mudou:
{
"id": "018f3c1e-7b2a-7c31-9d44-2f1a0b8e5c77:9c2b6a10-5e4d-4f38-b0c1-7a9d2e3f4b56",
"event_type": "sales-orders.update",
"timestamp": "2026-05-24T12:34:56.000Z",
"tenant_id": "550e8400-e29b-41d4-a716-446655440000",
"resource": {
"type": "sales-orders",
"id": "7f3d9c22-8a61-4e0b-9f52-3c8d1a7e6b04"
}
}| Campo | Descrição |
|---|---|
id | Mesmo valor do header x-biterp-webhook-id. Estável entre as retentativas do mesmo evento |
event_type | O evento assinado, no formato <recurso>.<ação> |
timestamp | Momento em que esta tentativa foi montada, em ISO 8601 UTC. Muda a cada retentativa |
tenant_id | A empresa em que a mudança ocorreu — útil se o seu sistema atende várias |
resource.type | O recurso, igual ao prefixo do event_type |
resource.id | O id a usar na hidratação via API |
O payload não traz o estado do recurso
Ele diz o que mudou, não como ficou. É uma decisão de projeto: o estado no momento da entrega pode já estar desatualizado, e um payload magro não vaza dados de negócio para um endpoint que porventura tenha sido comprometido. Busque o registro pela API quando precisar dos campos.
O evento de teste disparado pelo painel segue o mesmo formato, com event_type igual a webhook-endpoints.test e id no formato test_ seguido de um UUID.
Verificar a assinatura
A assinatura é calculada assim:
assinatura = HMAC_SHA256(signing_secret, "{timestamp}.{corpo_bruto}")
header = "sha256=" + hexadecimal(assinatura)Três detalhes que costumam quebrar a verificação:
- Use o corpo bruto, exatamente como chegou. Se o seu framework já transformou o JSON em objeto e você o serializa de novo, a menor diferença de espaço ou de ordem de chaves invalida o cálculo.
- A chave é o secret inteiro, incluindo o prefixo
whsec_. Não remova nada. - O separador é um ponto entre o timestamp e o corpo —
1774612345.{"id":...}.
Compare com uma função de tempo constante (crypto.timingSafeEqual, hmac.compare_digest) e rejeite timestamps antigos — uma tolerância de cinco minutos barra a repetição de uma requisição capturada.
Node.js (Express)
import crypto from "node:crypto"
import express from "express"
const app = express()
const SECRET = process.env.BITERP_WEBHOOK_SECRET // whsec_...
const TOLERANCE_SECONDS = 300
function isValid(rawBody, timestamp, signature) {
if (!timestamp || !signature) return false
const age = Math.abs(Math.floor(Date.now() / 1000) - Number(timestamp))
if (!Number.isFinite(age) || age > TOLERANCE_SECONDS) return false
const digest = crypto
.createHmac("sha256", SECRET)
.update(`${timestamp}.${rawBody}`)
.digest("hex")
const expected = Buffer.from(`sha256=${digest}`)
const received = Buffer.from(signature)
return (
expected.length === received.length && crypto.timingSafeEqual(expected, received)
)
}
// express.raw preserva o corpo original — express.json() o descartaria
app.post("/webhooks/biterp", express.raw({ type: "application/json" }), (req, res) => {
const rawBody = req.body.toString("utf8")
if (
!isValid(
rawBody,
req.get("x-biterp-webhook-timestamp"),
req.get("x-biterp-signature")
)
) {
return res.status(401).send("invalid signature")
}
const event = JSON.parse(rawBody)
enqueue(event) // processe fora do ciclo da requisição
res.status(200).send("ok")
})Python (Flask)
import hashlib
import hmac
import os
import time
from flask import Flask, abort, request
app = Flask(__name__)
SECRET = os.environ["BITERP_WEBHOOK_SECRET"].encode() # whsec_...
TOLERANCE_SECONDS = 300
@app.post("/webhooks/biterp")
def receive():
timestamp = request.headers.get("x-biterp-webhook-timestamp", "")
signature = request.headers.get("x-biterp-signature", "")
raw_body = request.get_data() # bytes, sem reserializar
try:
age = abs(int(time.time()) - int(timestamp))
except ValueError:
abort(401)
if age > TOLERANCE_SECONDS:
abort(401)
digest = hmac.new(SECRET, f"{timestamp}.".encode() + raw_body, hashlib.sha256).hexdigest()
if not hmac.compare_digest(f"sha256={digest}", signature):
abort(401)
enqueue(request.get_json()) # processe fora do ciclo da requisição
return "ok", 200Como responder
| Regra | Detalhe |
|---|---|
| Sucesso | Qualquer status 2xx. O corpo é irrelevante |
| Falha | Qualquer outro status, timeout ou erro de conexão agenda uma retentativa |
| Prazo | 10 segundos por tentativa. Depois disso a conexão é abortada e a entrega conta como falha |
| Assinatura inválida | Responda 401. Isso sinaliza um problema real de configuração, e o histórico no painel mostra o status |
Responda antes de processar. Grave o evento numa fila ou tabela, devolva 2xx e faça o trabalho pesado depois. Chamar a API do bitERP, gerar relatórios ou atualizar sistemas de terceiros dentro do handler é o caminho mais curto para estourar os 10 segundos e transformar entregas boas em retentativas.
Os primeiros 1.024 caracteres da sua resposta ficam registrados no histórico de entregas — útil para diagnosticar, então devolva uma mensagem de erro legível quando algo der errado do seu lado.
Erros comuns
| Sintoma | Causa provável |
|---|---|
| Assinatura nunca bate | O corpo foi reserializado (express.json(), body-parser) em vez de usar o bruto |
| Assinatura nunca bate | O prefixo whsec_ foi removido do secret |
| Assinatura nunca bate | O secret foi rotacionado no painel e a aplicação continua com o anterior |
| Funcionava e parou | Proxy ou CDN à frente do endpoint alterando o corpo, ou removendo os headers x-biterp-* |
| Entregas com timeout | Processamento síncrono dentro do handler |
| Nenhuma entrega chega | Endpoint desativado pelo circuit breaker — ver Entrega e retentativas |

