Pular para o conteúdo

Webhooks

Um projeto pode ter um webhook: uma URL sua que recebe um POST quando um job do projeto termina. Com ele, o seu sistema não precisa consultar GET /v1/jobs/{id} até o fim.

Na página do projeto, em Webhook, um admin informa a URL e clica em Ativar. O segredo de assinatura aparece uma vez: guarde-o junto do seu sistema. Na mesma área dá para:

  • Enviar teste: manda um evento ping, para conferir o recebimento e a assinatura;
  • Trocar segredo: as entregas seguintes passam a usar o segredo novo, na hora;
  • Remover: deixa de avisar, e as entregas na fila são descartadas;
  • ver as últimas entregas, com o estado e a resposta do seu sistema.

A URL precisa ser https e pública: endereços de rede interna são recusados, na ativação e a cada envio. Redirecionamentos não são seguidos.

evento Quando
job.concluido O job terminou e o resultado está pronto.
job.falhou O job falhou; a reserva voltou inteira.
job.cancelado O job foi cancelado antes de ganhar máquina; a reserva voltou.
ping Você pediu um teste.

O corpo é JSON. Nos eventos de job, job traz os mesmos campos de GET /v1/jobs/{id}:

{
"evento": "job.concluido",
"criado_em": "2026-11-01T14:03:11.204Z",
"job": {
"id": "6b0d1c3e-5f1a-4c2b-9a77-0e4f2d9b8c11",
"estado": "concluido",
"tipo": "simulacao",
"preco_micros": 14499640,
"criado_em": "2026-11-01T13:40:02.511Z",
"registro": { "…": "…" }
}
}
Cabeçalho O que é
BS-Webhook-Id O id da entrega. Uma entrega cuja resposta se perdeu é enviada de novo com o mesmo id: guarde os ids recebidos e ignore os repetidos.
BS-Webhook-Timestamp O momento do envio, em segundos desde 1970 (UTC).
BS-Webhook-Signature v1= seguido do HMAC-SHA256, em hexadecimal, de "<timestamp>.<corpo>" com o seu segredo.

Calcule o HMAC sobre os bytes do corpo como chegaram, antes de interpretar o JSON, e compare em tempo constante. Recuse também um timestamp com mais de 5 minutos, para que uma entrega gravada não possa ser reenviada depois.

import hashlib
import hmac
import time
def assinatura_valida(segredo: str, corpo: bytes, cabecalhos: dict[str, str]) -> bool:
timestamp = cabecalhos["BS-Webhook-Timestamp"]
if abs(time.time() - int(timestamp)) > 300:
return False
esperado = hmac.new(segredo.encode(), f"{timestamp}.".encode() + corpo, hashlib.sha256)
return hmac.compare_digest("v1=" + esperado.hexdigest(), cabecalhos["BS-Webhook-Signature"])
import { createHmac, timingSafeEqual } from "node:crypto";
export function assinaturaValida(segredo: string, corpo: Buffer, cabecalhos: Record<string, string>): boolean {
const timestamp = cabecalhos["bs-webhook-timestamp"];
if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false;
const esperado = "v1=" + createHmac("sha256", segredo).update(`${timestamp}.`).update(corpo).digest("hex");
const recebido = cabecalhos["bs-webhook-signature"] ?? "";
return esperado.length === recebido.length && timingSafeEqual(Buffer.from(esperado), Buffer.from(recebido));
}
  • Responda 2xx rápido. A entrega espera até 10 segundos pela resposta. Faça o trabalho pesado depois de responder.
  • Qualquer outra resposta, ou nenhuma, conta como falha. A entrega é repetida depois de 1 minuto, 5 minutos, 30 minutos, 2 horas e 12 horas. Depois da sexta tentativa, desistimos dela.
  • A ordem não é garantida, e um evento pode chegar mais de uma vez: use o BS-Webhook-Id para não processar duas vezes.
  • O webhook avisa, não substitui a fonte. Na dúvida, consulte GET /v1/jobs/{id}.