Webhooks
Reciba avisos en su servidor en lugar de consultar la API repetidamente.
Cómo configurar
- En el panel, registre una URL HTTPS y copie el secreto del webhook. La URL debe usar el puerto 443 y un dominio público: se rechazan IP, localhost, direcciones de red interna y dominios de BrazID.
- Responda con estado 2xx en hasta 10 segundos. Haga el trabajo pesado después de responder.
- Valide la firma de cada envío antes de confiar en el contenido.
Eventos
- usage.threshold_reached
- El consumo llegó al 80% o al 95% de la cuota del ciclo. Se dispara una vez por límite en cada ciclo.
- batch.completed
- Un lote terminó. El evento trae solo contadores y fechas, sin datos personales. Obtenga los resultados con
GET /v1/cpf/batch/{id}(oGET /v1/cnpj/batch/{id}cuando el evento traekind: "cnpj").
Campos de usage.threshold_reached
- idstring
- Identificador del evento (evt_…). Los reenvíos del mismo evento traen el mismo id: úselo para ignorar duplicados.
- typestring
- Siempre
usage.threshold_reached. - user_idstring
- Identificador de su cuenta.
- thresholdnumber
- Límite que se alcanzó: 80 o 95 (% de la cuota del ciclo).
- used_percentnumber
- Porcentaje de la cuota ya consumido cuando se generó el evento.
- monthly_creditsnumber
- Cuota de créditos de su plan en este ciclo.
- auto_charge_enabledboolean
- true: tras la cuota la API sigue respondiendo y cada consulta extra se cobra como excedente en la factura de la renovación. false: sin excedente, la API responde 402 (
CREDITS_EXHAUSTED) cuando se acaban los créditos. - period_startstring
- Inicio del ciclo al que se refiere el consumo (ISO 8601, UTC).
- period_endstring
- Fin del ciclo (ISO 8601, UTC). La cuota se renueva en esta fecha.
- created_atstring
- Momento en que se generó el evento (ISO 8601, UTC).
Validar la firma
- Cada envío trae el encabezado
x-brazid-signaturecon el formatot=<timestamp>,v1=<firma>. - La firma es el HMAC-SHA256, en hexadecimal, de la cadena
"<timestamp>.<cuerpo sin procesar>", calculado con el secreto del webhook. - Calcule el HMAC sobre el cuerpo exactamente como llegó, compárelo con v1 en tiempo constante y rechace timestamps muy antiguos.
Intentos y reintentos
- Cada evento tiene un id (el campo id del cuerpo y el encabezado x-brazid-event-id) que no cambia entre intentos. Úselo para ignorar duplicados.
- Lo enviamos en hasta 3 intentos: de inmediato, 5 minutos después y 1 hora después, con una pequeña variación aleatoria. El encabezado x-brazid-attempt trae el número del intento, y cada uno lleva una firma nueva.
- Cuenta como entregada una respuesta 2xx en hasta 10 segundos. Reintentamos cuando no hay respuesta y en los estados 5xx, 408 y 429 (respetando
Retry-After, hasta 1 hora). Los demás 4xx no se reintentan. - Si los 3 intentos fallan, el evento aparece como Falló en el panel y puede reenviarlo a mano.
- Cuando 10 eventos seguidos fallan, desactivamos el webhook y le avisamos por correo. Reactive el envío en el panel después de corregir la URL.
- El historial de entregas se conserva por 30 días.
usage.threshold_reached
1{2 "id": "evt_Xk3p9QmT2vHf",3 "type": "usage.threshold_reached",4 "user_id": "9b2f…",5 "threshold": 80,6 "used_percent": 80,7 "monthly_credits": 200,8 "auto_charge_enabled": true,9 "period_start": "2026-10-13T00:00:00.000Z",10 "period_end": "2026-11-12T00:00:00.000Z",11 "created_at": "2026-10-25T09:41:07.000Z"12}
batch.completed
1{2 "id": "evt_c7LwR1sYb0Ae",3 "type": "batch.completed",4 "batch_id": "0c1e7f3a-5b2d-4e8a-9c41-6d3f0a8b7e12",5 "total": 2,6 "found": 1,7 "not_found": 1,8 "invalid": 0,9 "errors": 0,10 "insufficient_credits": 0,11 "created_at": "2026-10-16T14:02:11.000Z",12 "completed_at": "2026-10-16T14:02:13.000Z"13}
Node.js
1import { createHmac, timingSafeEqual } from 'node:crypto';2 3export function isValidSignature(rawBody, header, secret) {4 const parts = Object.fromEntries(header.split(',').map((p) => p.split('=')));5 const expected = createHmac('sha256', secret)6 .update(`${parts.t}.${rawBody}`)7 .digest('hex');8 9 const a = Buffer.from(expected);10 const b = Buffer.from(parts.v1 ?? '');11 const fresh = Math.abs(Date.now() / 1000 - Number(parts.t)) < 300;12 13 return fresh && a.length === b.length && timingSafeEqual(a, b);14}
1import hmac2import hashlib3import time4 5 6def is_valid_signature(raw_body: bytes, header: str, secret: str) -> bool:7 parts = dict(p.split("=", 1) for p in header.split(","))8 signed = parts["t"].encode() + b"." + raw_body9 expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()10 11 fresh = abs(time.time() - int(parts["t"])) < 30012 return fresh and hmac.compare_digest(expected, parts.get("v1", ""))