Skip to main content

Continuous Monitoring

This guide explains how to implement a continuous monitoring system for every company in your portfolio to detect major events (collective procedures, officer changes, deregistrations, etc.).

Goal

Get automatically alerted when a company experiences:

  • A collective procedure (recovery, liquidation)
  • A change of company officer
  • Deregistration or closure
  • A BODACC publication
  • Detected financial distress

Monitoring architecture

Event-to-alert pipeline

Run automated syncs, compare changes, and ship webhooks back to your stack.

Your Application

Step 1

  • Register the companies to monitor
  • Store webhook payloads & react (emails, tasks, flags)
  • Display monitoring status to internal teams

Webhooks & alerts

Step 2

  • DATA_SYNC_SUCCESS, FETCH_ERROR, AUTH_ERROR
  • Push notifications back to your system
  • Use signatures to verify authenticity

Qard Monitoring core

Step 3

  • Scheduled sync scheduler
  • Diff engine: compare previous vs new data
  • Change detector triggers webhooks

Official sources

Step 4

  • INPI & RNM registries
  • INSEE updates
  • BODACC publications & other feeds

Data under watch

Data typeDetected changes
Company ProfileDeregistration, closure, address change
Company OfficersNew appointments, resignations, removals
Collective ProcedureNewly opened insolvency procedures
Beneficial OwnersBeneficial owner updates
Background Check V2Risk level changes
TimelineAggregated list of events

Implementation steps

Step 1: Configure the webhooks

Set up the webhooks that will deliver notifications:

PUT /api/v6/webhooks/DATA_SYNC_SUCCESS
{
"url": "https://mon-app.com/webhooks/qard/sync-success"
}
PUT /api/v6/webhooks/DATA_SYNC_FETCH_ERROR
{
"url": "https://mon-app.com/webhooks/qard/sync-error"
}
Signature secret

Configure a secret to validate the authenticity of incoming webhooks:

PUT/api/v6/profile/webhook-secret

Response:

{
"secret": "QQF909SvZ6AKFntP2RNiDdlKIQPuY8CwVoCMsoJVDLqWywDeRjtj..."
}

Step 2: Validate incoming webhooks

const crypto = require('crypto');

function validateWebhook(requestUrl, signature, secret) {
const expectedSignature = crypto
.createHmac('sha256', secret)
.update(requestUrl)
.digest('base64');

return `sha256=${expectedSignature}` === signature;
}

// In your webhook handler
app.get('/webhooks/qard/sync-success', (req, res) => {
const signature = req.headers['x-webhook-signature'];
const fullUrl = `${req.protocol}://${req.get('host')}${req.originalUrl}`;

if (!validateWebhook(fullUrl, signature, WEBHOOK_SECRET)) {
return res.status(401).send('Invalid signature');
}

const { eventType, resourceId, userId, date } = req.query;

// Handle the event
handleSyncSuccess(userId, resourceId);

res.status(200).send('OK');
});

Step 3: Enable the monitoring system

PUT /api/v6/providers/company_legal_fr/settings
{
"auto_connect": true,
"monitoring_enabled": true,
"monitoring_frequency": "daily"
}

Step 4: Register the companies you want to monitor

For every company in your portfolio:

POST /api/v6/users/legal
{
"name": "Client ABC SARL",
"siren": "123456789",
"group": "monitoring-portfolio"
}
POST /api/v6/users/{userId}/data-connections
{
"requested_data_types": [
"COMPANY_PROFILE",
"COMPANY_OFFICER",
"COLLECTIVE_PROCEDURE",
"BENEFICIAL_OWNER",
"BACKGROUND_CHECK_V2",
"TIMELINE"
],
"provider_name": "company_legal_fr"
}

Step 5: Initial synchronization

POST /api/v6/users/{userId}/sync
{
"data_types": [
"COMPANY_PROFILE",
"COMPANY_OFFICER",
"COLLECTIVE_PROCEDURE",
"BENEFICIAL_OWNER",
"BACKGROUND_CHECK_V2",
"TIMELINE"
]
}

Step 6: Process the notifications

DATA_SYNC_SUCCESS webhook structure

GET https://mon-app.com/webhooks/qard/sync-success
?eventType=DATA_SYNC_SUCCESS
&resourceId=<dataSyncId>
&userId=<userId>
&date=1609459200
&nonce=ff3a4945c82d0594e75b4588ddd416f0...

Handling example

async function handleSyncSuccess(userId, dataSyncId) {
// 1. Retrieve the DataSync details
const dataSync = await qardApi.getDataSync(userId, dataSyncId);

// 2. Branch depending on the synchronized data type
switch (dataSync.data_type) {
case 'COMPANY_PROFILE':
await checkProfileChanges(userId);
break;

case 'COLLECTIVE_PROCEDURE':
await checkNewProcedures(userId);
break;

case 'COMPANY_OFFICER':
await checkOfficerChanges(userId);
break;

case 'BACKGROUND_CHECK_V2':
await checkRiskChanges(userId);
break;
}
}

async function checkNewProcedures(userId) {
const procedures = await qardApi.getCollectiveProcedures(userId);

// Keep only recent procedures (< 30 days)
const recentProcedures = procedures.filter(p => {
const daysSince = (Date.now() - new Date(p.date).getTime()) / (1000 * 60 * 60 * 24);
return daysSince < 30;
});

if (recentProcedures.length > 0) {
await sendAlert({
type: 'COLLECTIVE_PROCEDURE',
userId,
severity: 'HIGH',
data: recentProcedures
});
}
}

async function checkRiskChanges(userId) {
const currentCheck = await qardApi.getBackgroundCheckV2(userId);
const previousCheck = await getStoredBackgroundCheck(userId);

// Compare statuses
if (previousCheck?.acceptable === 'OK' && currentCheck.acceptable === 'NOK') {
await sendAlert({
type: 'RISK_DEGRADATION',
userId,
severity: 'CRITICAL',
previousReasons: previousCheck.reasons,
currentReasons: currentCheck.reasons
});
}

// Store the new result
await storeBackgroundCheck(userId, currentCheck);
}

Timeline: aggregated view of events

The Timeline endpoint provides a chronological list of every event:

GET/api/v6/users/{userId}/timeline

Response:

{
"total": 3,
"per_page": 20,
"current_page": 1,
"last_page": 1,
"result": [
{
"date": "2025-10-30",
"events": [
{
"type": "Act",
"titles": [
"Document de synthèse des bénéficiaires effectifs signé",
"Document relatif au bénéficiaire effectif"
],
"file_id": "dd75061c-c153-4b9f-906f-1b0e49f3d134"
},
{
"type": "Act",
"titles": [
"PV ayant décidé et constaté la modification enregistrée, certifié conforme par le représentant légal"
],
"file_id": "1bc86b69-1e5a-4795-ba76-c126c68262b7"
}
]
},
{
"date": "2025-08-12",
"events": [
{
"type": "FinancialStatement",
"statement_type": "STANDALONE",
"closing_date": "2024-12-31",
"file_id": "80f0d621-d29a-4875-9c5c-59db2a8b9793",
"revenue": null,
"net_profit": null
}
]
},
{
"date": "2024-07-23",
"events": [
{
"type": "Observation",
"description": "Modification relative aux dirigeants xxxxx Arnaud nom d'usage : xxxx n'est plus directeur général xxxx nom d'usage : xxxx devient directeur général à compter du 30/06/2024"
},
{
"type": "Observation",
"description": "Modification relative aux dirigeants xxxx nom d'usage : xxxx n'est plus directeur général xxxx nom d'usage : xxxxx devient directeur général à compter du 30/06/2024"
}
]
}
]
}

Handling synchronization errors

async function handleSyncError(userId, dataSyncId) {
const dataSync = await qardApi.getDataSync(userId, dataSyncId);

switch (dataSync.status) {
case 'AUTH_ERROR':
// The user must re-authenticate
await notifyReauthRequired(userId, dataSync.provider_name);
break;

case 'FETCH_ERROR':
// Temporary error, reschedule
await scheduleRetry(userId, dataSync.data_type, { delay: '1h' });
break;

case 'INTERNAL_ERROR':
// Qard internal error, contact support
await logInternalError(userId, dataSyncId);
break;
}
}

Best practices

Data typeFrequencyRationale
Collective ProcedureDailyCatch distress events quickly
Company ProfileWeeklyRare updates
Company OfficersWeeklyOccasional changes
Background Check V2DailyAggregates multiple sources
TimelineDailyConsolidated view

Managing volume

When you monitor many companies, stagger the synchronizations:

async function scheduleBatchSync(userIds, dataTypes) {
const BATCH_SIZE = 100;
const DELAY_BETWEEN_BATCHES = 60000; // 1 minute

for (let i = 0; i < userIds.length; i += BATCH_SIZE) {
const batch = userIds.slice(i, i + BATCH_SIZE);

await Promise.all(batch.map(userId =>
qardApi.sync(userId, { data_types: dataTypes })
));

if (i + BATCH_SIZE < userIds.length) {
await sleep(DELAY_BETWEEN_BATCHES);
}
}
}

Retry strategy in case of failure

const RETRY_CONFIG = {
maxAttempts: 3,
delays: [60000, 300000, 900000] // 1min, 5min, 15min
};

async function syncWithRetry(userId, dataTypes, attempt = 0) {
try {
await qardApi.sync(userId, { data_types: dataTypes });
} catch (error) {
if (error.status === 409) {
// Sync already running, wait
await sleep(30000);
return syncWithRetry(userId, dataTypes, attempt);
}

if (attempt < RETRY_CONFIG.maxAttempts) {
await sleep(RETRY_CONFIG.delays[attempt]);
return syncWithRetry(userId, dataTypes, attempt + 1);
}

throw error;
}
}

Available webhooks

Event TypeDescription
DATA_SYNC_QUEUEDSynchronization queued
DATA_SYNC_FETCHINGSynchronization running
DATA_SYNC_SUCCESSSynchronization succeeded
DATA_SYNC_FETCH_ERRORData retrieval error
DATA_SYNC_AUTH_ERRORAuthentication error
DATA_CONNECTION_CONNECTEDConnection established
DATA_CONNECTION_AUTH_ERRORConnection authentication error

See also