Skip to content

Webhooks

Get notified on your server instead of polling the API.

How to set up

  1. In the dashboard, register an HTTPS URL and copy the webhook secret. The URL must use port 443 and a public domain: IP addresses, localhost, internal network addresses and BrazID domains are rejected.
  2. Answer with a 2xx status within 10 seconds. Do the heavy work after replying.
  3. Validate each delivery’s signature before trusting the content.

Events

usage.threshold_reached
Usage reached 80% or 95% of the cycle allowance. Fires once per threshold in each cycle.
batch.completed
A batch finished. The event carries only counters and dates, no personal data. Fetch the results with GET /v1/cpf/batch/{id} (or GET /v1/cnpj/batch/{id} when the event carries kind: "cnpj").

usage.threshold_reached fields

idstring
Event identifier (evt_…). Resends of the same event carry the same id: use it to ignore duplicates.
typestring
Always usage.threshold_reached.
user_idstring
Your account identifier.
thresholdnumber
The threshold that was reached: 80 or 95 (% of the cycle allowance).
used_percentnumber
Percentage of the allowance already used when the event was generated.
monthly_creditsnumber
Your plan credit allowance for this cycle.
auto_charge_enabledboolean
true: after the allowance the API keeps answering and each extra lookup is billed as overage on the renewal invoice. false: no overage, the API answers 402 (CREDITS_EXHAUSTED) when credits run out.
period_startstring
Start of the cycle the usage refers to (ISO 8601, UTC).
period_endstring
End of the cycle (ISO 8601, UTC). The allowance renews on this date.
created_atstring
When the event was generated (ISO 8601, UTC).

Validating the signature

  • Each delivery carries the x-brazid-signature header in the format t=<timestamp>,v1=<signature>.
  • The signature is the HMAC-SHA256, in hexadecimal, of the string "<timestamp>.<raw body>", computed with the webhook secret.
  • Compute the HMAC over the body exactly as received, compare it with v1 in constant time and reject timestamps that are too old.

Attempts and retries

  • Each event has an id (the id field in the body and the x-brazid-event-id header) that stays the same across attempts. Use it to ignore duplicates.
  • We send it in up to 3 attempts: right away, 5 minutes later and 1 hour later, with a small random variation. The x-brazid-attempt header carries the attempt number, and each attempt has a fresh signature.
  • A 2xx response within 10 seconds counts as delivered. We retry when there is no response and on 5xx, 408 and 429 statuses (honoring Retry-After, up to 1 hour). Other 4xx responses are not retried.
  • If all 3 attempts fail, the event shows as Failed in the dashboard and you can resend it by hand.
  • When 10 events in a row fail, we turn the webhook off and email you. Turn sending back on in the dashboard after fixing the URL.
  • Delivery history is kept for 30 days.

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}