Saltar al contenido

Webhooks

Reciba avisos en su servidor en lugar de consultar la API repetidamente.

Cómo configurar

  1. 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.
  2. Responda con estado 2xx en hasta 10 segundos. Haga el trabajo pesado después de responder.
  3. 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} (o GET /v1/cnpj/batch/{id} cuando el evento trae kind: "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-signature con el formato t=<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}