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 or IDV risk change.
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 } = webhook;
switch (verificationStatus) {
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;
}
}
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
userId- Description
The user ID of the entity in your system.
- Name
onBehalfUid- Description
The UID of the onBehalf account.
- 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
idvRisk- Description
The IDV risk of the entity.
- Name
verificationStatus- Description
The verification status.
- Name
submissionStatus- Description
The submission status.
- Name
timestamp- Description
The timestamp when the webhook was sent.
Example payload
{
"userId": "user-123",
"onBehalfUid": "onBehalf-123",
"trace": "test-trace",
"submissionId": "test-submission-id",
"name": "My Company",
"country": "AUS",
"type": "company",
"subType": "none",
"idvRisk": "high",
"verificationStatus": "pending",
"submissionStatus": "submitted",
"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)