Webhooks
In this guide, we will look at how to register and consume webhooks to integrate your app with bronID. With webhooks, your app can know when something happens in bronID, such as a verification status change, risk update, or screening completion.
What are Webhooks?
Webhooks are real-time notifications that bronID sends to your application whenever something important changes in your verifications. Rather than repeatedly polling the bronID API to check if a verification is complete, webhooks push updates directly to your server as soon as events occur. This enables you to respond immediately to status changes and provide timely feedback to your customers throughout the verification process.
You configure the URL at which you would like to receive the webhooks in the bronID Portal. bronID will send HTTP POST requests to this endpoint whenever a verification transitions between states, allowing your application to stay synchronized with the verification lifecycle.
How to Use Webhooks
1. Configure Your Webhook Endpoints
Set up a publicly accessible HTTPS endpoint where your application can receive POST requests from bronID. Configure this URL in the bronID Portal under Settings » Developers » API keys » Webhook urls section.
Your endpoint should be able to:
- Accept HTTP POST requests
- Return a
200 OKresponse to acknowledge receipt - Process webhook payloads asynchronously to avoid timeout issues
2. Listen for Webhook Notifications
When bronID sends a webhook, your endpoint receives the payload containing the current verification status.
Based on the verificationStatus field, you should update your application state and communicate
the appropriate next steps to your customer.
Example webhook handling logic
Handle webhook notifications
// Webhook endpoint handler in your application
app.post('/webhooks/bronid', async (req, res) => {
const webhook = req.body;
// Acknowledge receipt immediately
res.status(200).send('OK');
// Process webhook asynchronously
await processWebhook(webhook);
});
async function processWebhook(webhook) {
const { trace, verificationStatus, screening, idvRisk } = webhook;
// Check screening results if present
if (screening?.watchlist) {
const { status, totalHits } = screening.watchlist;
if (status === 'hits_found') {
await flagForReview(trace, `Watchlist screening found ${totalHits} hit(s)`);
}
}
switch (verificationStatus) {
case 'new':
// This webhook is currently not sent
break;
case 'pending':
// Inform customer verification is being processed
await notifyCustomer(trace, 'Your submission is being reviewed');
break;
case 'verified':
// Notify customer of successful verification
await notifyCustomer(trace, 'Verification complete.');
await updateCustomerStatus(trace, 'verified');
break;
case 'rejected':
// Inform customer verification was unsuccessful
await notifyCustomer(trace, 'Verification unsuccessful. Please review the submitted information.');
break;
case 'info':
// Retrieve detailed information about what's required
const details = await getVerificationDetails(trace);
await handleInfoRequest(trace, details);
break;
case 'error':
// Handle error scenario
const errorDetails = await getVerificationDetails(trace);
await handleError(trace, errorDetails);
break;
case 'locked':
// Too many failed attempts — no further verification attempts will be processed
await notifyCustomer(trace, 'Verification locked due to too many failed attempts.');
await updateCustomerStatus(trace, 'locked');
break;
}
}
3. Retrieve Detailed Verification Data
When you receive a webhook notification, the payload contains only summary information about the verification state.
To access comprehensive details – including specific information requests, rejection reasons, error details,
or the complete entity data – use the verification details endpoint by submitting a GET request to /v5/verifications/:trace with the trace identifier from the webhook payload.
Webhook schema
- Name
version- Description
The webhook payload version, tracking the API namespace. Always
"v5"for v5 webhooks. Use it to branch your handler if the payload shape ever changes in a future API version.
- Name
userId- Description
The user ID of the entity in your system.
- Name
onBehalfUid- Description
The UID of the onBehalf account.
- Name
tags- Description
Array of tag labels currently attached to the verification (e.g.
["region:apac", "tier-1 >>> gold"]). Always present; an empty array if no tags have been set.
- Name
trace- Description
The trace of the verification.
- Name
submissionId- Description
The submission ID of the verification.
- Name
name- Description
The name of the entity.
- Name
country- Description
The ISO3 country code of the entity.
- Name
type- Description
The type of the entity.
- Name
subType- Description
The sub-type of the entity.
- Name
inputRisk- Description
The risk level provided by the customer when submitting the verification. Optional.
- Name
idvRisk- Description
The calculated IDV risk level, based on country risk, screening results, stakeholder risk, and input risk. Optional — absent until risk is calculated.
- Name
screening- Description
Screening summary. Present once watchlist screening has been performed. Contains
screening.watchlistwithstatus("clear"or"hits_found"),totalHits,lastScreenedAt(Unix timestamp ms), andlastScreenedTime.
- Name
verificationStatus- Description
The verification status. For stakeholder webhooks this may additionally be
removed, emitted when an individual is removed from the entity they were part of.
- Name
submissionStatus- Description
The submission status.
- Name
timestamp- Description
The timestamp when the webhook was sent.
- Name
parentTrace- Description
Stakeholder webhooks only. The
traceof the encapsulating entity verification. Absent on entity and standalone-individual webhooks.
- Name
parentUserId- Description
Stakeholder webhooks only. The
userIdof the encapsulating entity, when set.
- Name
roles- Description
Stakeholder webhooks only. The full set of roles the individual holds within the entity (e.g.
["director", "shareholder"]). An individual requested under multiple roles reports all of them.
Example payload (verified with screening)
{
"version": "v5",
"userId": "user-123",
"onBehalfUid": "onBehalf-123",
"trace": "test-trace",
"submissionId": "test-submission-id",
"name": "My Company",
"country": "AUS",
"type": "company",
"subType": "none",
"inputRisk": "high",
"idvRisk": "high",
"screening": {
"watchlist": {
"status": "clear",
"totalHits": 0,
"lastScreenedAt": 1767004800000,
"lastScreenedTime": "29/12/2025 21:40:00"
}
},
"verificationStatus": "verified",
"submissionStatus": "submitted",
"tags": ["region:apac", "tier-1 >>> gold"],
"timestamp": 1767004789403
}
Stakeholder webhooks
When an individual is verified as part of an entity — a stakeholder such as a director or beneficial owner — bronID sends a dedicated webhook for that individual, in addition to the entity's own webhook. The stakeholder webhook is keyed on the individual:
traceis the individual stakeholder's own trace (the same identifier as that person's eKYC), not the entity's.parentTrace,parentUserId, androleslink the individual back to the encapsulating entity.verificationStatusmay beremovedwhen the individual is removed from the entity.
The entity itself still receives its own webhook (keyed on the entity trace) as its aggregate status changes. To correlate the two, match a stakeholder webhook's parentTrace to the entity webhook's trace. Both formats are signed identically (see Security).
Example payload (verified stakeholder)
{
"version": "v5",
"userId": null,
"onBehalfUid": "onBehalf-123",
"trace": "stakeholder-trace",
"parentTrace": "entity-trace",
"parentUserId": "user-123",
"roles": ["director", "shareholder"],
"submissionId": "entity-trace",
"name": "Rachel Mills",
"country": "AUS",
"type": "individual",
"subType": "biometric",
"inputRisk": "low",
"idvRisk": "low",
"screening": null,
"verificationStatus": "verified",
"submissionStatus": "submitted",
"tags": [],
"timestamp": 1767004789403
}
Security
To know for sure that a webhook was, in fact, sent by bronID instead of a malicious actor, you can verify the request signature. Each webhook request contains the following headers:
X-BronID-Signature- Contains the HMAC-SHA256 signature in the formatsha256=<hex-encoded-hash>X-BronID-Signature-version- Contains the signature version (currently2)
You can verify this signature by using your webhook signing key, which you can get from the Settings » Developers » API keys » Secret signing keys section. The signature is an HMAC hash of the request payload hashed using your secret key. Here is an example of how to verify the signature in your app:
Verifying a request
import crypto from 'crypto';
function verifyWebhook(payload: string, signature: string, secret: string): boolean {
// 1. Validate signature format
// The signature must start with "sha256=" to indicate the hashing algorithm used.
// This prevents algorithm confusion attacks and ensures we're comparing like-for-like.
if (!signature.startsWith('sha256=')) {
return false;
}
// 2. Extract the hash value
// Remove the "sha256=" prefix to get the raw hex-encoded hash for comparison.
const receivedHash = signature.replace('sha256=', '');
// 3. Compute the expected hash
// Using HMAC-SHA256: a keyed hash that combines the payload with our secret.
// Only someone with the secret can generate a valid signature.
const expectedHash = crypto.createHmac('sha256', secret).update(payload).digest('hex');
// 4. Compare signatures using timing-safe comparison
// Regular string comparison (===) is vulnerable to timing attacks where an attacker
// can measure response times to guess the signature character by character.
// timingSafeEqual compares in constant time regardless of where differences occur.
try {
if (!crypto.timingSafeEqual(Buffer.from(expectedHash), Buffer.from(receivedHash))) {
return false;
}
} catch {
// timingSafeEqual throws if buffer lengths differ.
// This is a sign of an invalid signature, so we reject it.
return false;
}
// 5. Validate timestamp to prevent replay attacks
// Without this check, an attacker who intercepts a valid signed request
// could replay it indefinitely. The timestamp ensures requests expire.
const data = JSON.parse(payload);
const timestamp = data.timestamp;
const age = Date.now() - timestamp;
const tolerance = 5 * 60 * 1000; // 5 minutes in milliseconds
// Reject if timestamp is too old (expired) or too far in the future (clock skew attack)
if (Math.abs(age) > tolerance) {
return false;
}
return true;
}
You should check:
- Timestamp within ±5 minutes (to prevent replay attacks)
- Signature matches (to prevent tampering)
- Reject replays (to prevent duplicate deliveries)