biterp

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

HeaderExemploPara que serve
content-typeapplication/jsonO corpo é sempre JSON
user-agentbitERP-Webhooks/1.0Identifica a origem (não use como autenticação)
x-biterp-webhook-id018f...:9c2b...Identificador único do evento — a chave da sua idempotência
x-biterp-webhook-timestamp1774612345Momento da assinatura, em segundos desde a época Unix
x-biterp-signaturesha256=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"
    }
}
CampoDescrição
idMesmo valor do header x-biterp-webhook-id. Estável entre as retentativas do mesmo evento
event_typeO evento assinado, no formato <recurso>.<ação>
timestampMomento em que esta tentativa foi montada, em ISO 8601 UTC. Muda a cada retentativa
tenant_idA empresa em que a mudança ocorreu — útil se o seu sistema atende várias
resource.typeO recurso, igual ao prefixo do event_type
resource.idO 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:

  1. 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.
  2. A chave é o secret inteiro, incluindo o prefixo whsec_. Não remova nada.
  3. 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", 200

Como responder

RegraDetalhe
SucessoQualquer status 2xx. O corpo é irrelevante
FalhaQualquer outro status, timeout ou erro de conexão agenda uma retentativa
Prazo10 segundos por tentativa. Depois disso a conexão é abortada e a entrega conta como falha
Assinatura inválidaResponda 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

SintomaCausa provável
Assinatura nunca bateO corpo foi reserializado (express.json(), body-parser) em vez de usar o bruto
Assinatura nunca bateO prefixo whsec_ foi removido do secret
Assinatura nunca bateO secret foi rotacionado no painel e a aplicação continua com o anterior
Funcionava e parouProxy ou CDN à frente do endpoint alterando o corpo, ou removendo os headers x-biterp-*
Entregas com timeoutProcessamento síncrono dentro do handler
Nenhuma entrega chegaEndpoint desativado pelo circuit breaker — ver Entrega e retentativas

Nesta página