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
- Register the companies to monitor
- Store webhook payloads & react (emails, tasks, flags)
- Display monitoring status to internal teams
Webhooks & alerts
- DATA_SYNC_SUCCESS, FETCH_ERROR, AUTH_ERROR
- Push notifications back to your system
- Use signatures to verify authenticity
Qard Monitoring core
- Scheduled sync scheduler
- Diff engine: compare previous vs new data
- Change detector triggers webhooks
Official sources
- INPI & RNM registries
- INSEE updates
- BODACC publications & other feeds
Data under watch
| Data type | Detected changes |
|---|---|
| Company Profile | Deregistration, closure, address change |
| Company Officers | New appointments, resignations, removals |
| Collective Procedure | Newly opened insolvency procedures |
| Beneficial Owners | Beneficial owner updates |
| Background Check V2 | Risk level changes |
| Timeline | Aggregated 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-secretResponse:
{
"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}/timelineResponse:
{
"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
Recommended synchronization frequency
| Data type | Frequency | Rationale |
|---|---|---|
| Collective Procedure | Daily | Catch distress events quickly |
| Company Profile | Weekly | Rare updates |
| Company Officers | Weekly | Occasional changes |
| Background Check V2 | Daily | Aggregates multiple sources |
| Timeline | Daily | Consolidated 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 Type | Description |
|---|---|
DATA_SYNC_QUEUED | Synchronization queued |
DATA_SYNC_FETCHING | Synchronization running |
DATA_SYNC_SUCCESS | Synchronization succeeded |
DATA_SYNC_FETCH_ERROR | Data retrieval error |
DATA_SYNC_AUTH_ERROR | Authentication error |
DATA_CONNECTION_CONNECTED | Connection established |
DATA_CONNECTION_AUTH_ERROR | Connection authentication error |
See also
- Webhooks - Complete webhook documentation
- Synchronization - How synchronization works
- Timeline - Timeline data format
- Collective Procedure - Collective procedure format