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 OK response 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 format sha256=<hex-encoded-hash>
  • X-BronID-Signature-version - Contains the signature version (currently 2)

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)

Was this page helpful?