Webhooks
Get notified on your server instead of polling the API.
How to set up
- 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.
- Answer with a 2xx status within 10 seconds. Do the heavy work after replying.
- 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}(orGET /v1/cnpj/batch/{id}when the event carrieskind: "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-signatureheader in the formatt=<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}
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", ""))