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 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, 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.watchlist with status ("clear" or "hits_found"), totalHits, lastScreenedAt (Unix timestamp ms), and lastScreenedTime.

  • 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 trace of the encapsulating entity verification. Absent on entity and standalone-individual webhooks.

  • Name
    parentUserId
    Description

    Stakeholder webhooks only. The userId of 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:

  • trace is the individual stakeholder's own trace (the same identifier as that person's eKYC), not the entity's.
  • parentTrace, parentUserId, and roles link the individual back to the encapsulating entity.
  • verificationStatus may be removed when 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 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?