Real-time Bedrijfsdata Updates met Webhooks en Polling
· KVKBase Team

Real-time Bedrijfsdata Updates met Webhooks en Polling

Hoe je bedrijfsdata actueel houdt met polling, cache-invalidatie en periodieke hervalidatie. Praktische patronen voor je applicatie.

webhooksapireal-time

Real-time Bedrijfsdata Updates met Webhooks en Polling

Je hebt bedrijfsgegevens opgehaald bij de KVK en opgeslagen in je database. Maar hoe houd je die data actueel? Bedrijven verhuizen, veranderen van naam of worden uitgeschreven. Als je geen strategie hebt om wijzigingen op te vangen, werkt je applicatie binnen een paar maanden met verouderde data.

In dit artikel bespreken we de twee hoofdstrategieen — polling en webhooks — en geven we praktische implementatiepatronen voor het actueel houden van bedrijfsdata.

Het probleem: data veroudert

Bedrijfsgegevens zijn niet statisch. In een willekeurig jaar gebeurt het volgende in het handelsregister:

  • Duizenden bedrijven veranderen van adres
  • Bedrijven wijzigen hun handelsnaam
  • Nieuwe bedrijven worden ingeschreven
  • Bedrijven worden uitgeschreven of ontbonden
  • Rechtsvormen veranderen (eenmanszaak wordt BV)

Als je 10.000 bedrijfsrecords in je database hebt, zijn er na zes maanden gegarandeerd honderden records die niet meer kloppen.

Strategie 1: Polling

Polling is de eenvoudigste aanpak: je controleert periodiek of de data van je opgeslagen bedrijven nog actueel is.

Basisimplementatie

const REFRESH_INTERVAL_DAYS = 7; // Hervalideer elke 7 dagen

async function refreshStaleCompanies() {
  const cutoff = new Date();
  cutoff.setDate(cutoff.getDate() - REFRESH_INTERVAL_DAYS);

  // Haal bedrijven op die langer dan 7 dagen niet zijn gecontroleerd
  const staleCompanies = await db.companies.findWhere(
    'last_verified_at < ? OR last_verified_at IS NULL',
    cutoff
  );

  console.log(`${staleCompanies.length} bedrijven moeten worden gehervalideerd`);

  for (const company of staleCompanies) {
    try {
      const fresh = await kvkbase.lookup(company.kvkNumber);
      const changes = detectChanges(company, fresh);

      if (changes.length > 0) {
        await db.companies.update(company.id, {
          tradeName: fresh.tradeName,
          address: fresh.address,
          isActive: fresh.isActive,
          lastVerifiedAt: new Date()
        });

        await logChanges(company.kvkNumber, changes);
      } else {
        // Geen wijzigingen, alleen de verificatiedatum bijwerken
        await db.companies.update(company.id, {
          lastVerifiedAt: new Date()
        });
      }
    } catch (error) {
      console.error(`Hervalidatie mislukt voor ${company.kvkNumber}:`, error);
    }

    // Pauze om rate limits te respecteren
    await sleep(200);
  }
}

function detectChanges(old, fresh) {
  const changes = [];

  if (old.tradeName !== fresh.tradeName) {
    changes.push({ field: 'tradeName', old: old.tradeName, new: fresh.tradeName });
  }
  if (old.isActive !== fresh.isActive) {
    changes.push({ field: 'isActive', old: old.isActive, new: fresh.isActive });
  }
  // Vergelijk adresvelden...

  return changes;
}

Slim plannen

Niet alle bedrijven hoeven even vaak gecontroleerd te worden. Prioriteer op basis van activiteit:

function getRefreshPriority(company) {
  // Actieve klanten: elke 3 dagen
  if (company.hasRecentOrders) return 3;

  // Bedrijven met openstaande facturen: dagelijks
  if (company.hasOpenInvoices) return 1;

  // Inactieve klanten: elke 30 dagen
  if (!company.hasRecentActivity) return 30;

  // Standaard: elke 7 dagen
  return 7;
}

async function refreshByPriority() {
  const companies = await db.companies.findAll();

  for (const company of companies) {
    const priority = getRefreshPriority(company);
    const daysSinceRefresh = daysBetween(company.lastVerifiedAt, new Date());

    if (daysSinceRefresh >= priority) {
      await refreshCompany(company);
    }
  }
}

Cron-job opzetten

Plan de polling als een cron-job die dagelijks draait:

# crontab -e
# Elke nacht om 03:00 de hervalidatie draaien
0 3 * * * node /app/scripts/refresh-companies.js

Of met een task scheduler in Node.js:

import cron from 'node-cron';

cron.schedule('0 3 * * *', async () => {
  console.log('Start bedrijfsdata hervalidatie...');
  await refreshStaleCompanies();
  console.log('Hervalidatie afgerond');
});

Strategie 2: Webhooks

Webhooks zijn het omgekeerde van polling: in plaats van dat jij periodiek controleert, stuurt de databron een bericht naar jouw applicatie wanneer er iets verandert.

Webhook-endpoint opzetten

// Express endpoint voor webhook-notificaties
app.post('/webhooks/company-updates', async (req, res) => {
  // Verifieer de webhook-handtekening
  const signature = req.headers['x-webhook-signature'];
  if (!verifySignature(req.body, signature)) {
    return res.status(401).json({ error: 'Invalid signature' });
  }

  const { kvkNumber, eventType, data } = req.body;

  switch (eventType) {
    case 'company.updated':
      await handleCompanyUpdate(kvkNumber, data);
      break;
    case 'company.deregistered':
      await handleCompanyDeregistered(kvkNumber);
      break;
    case 'company.address_changed':
      await handleAddressChange(kvkNumber, data);
      break;
    default:
      console.log(`Onbekend event type: ${eventType}`);
  }

  // Altijd 200 terugsturen om te bevestigen
  res.status(200).json({ received: true });
});

Webhook-beveiliging

Webhooks moeten altijd geverifieerd worden. Vertrouw nooit blindelings op binnenkomende verzoeken:

const crypto = require('crypto');

function verifySignature(payload, signature) {
  const secret = process.env.WEBHOOK_SECRET;
  const expected = crypto
    .createHmac('sha256', secret)
    .update(JSON.stringify(payload))
    .digest('hex');

  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expected)
  );
}

Idempotentie

Webhooks kunnen meerdere keren worden afgeleverd. Zorg dat je handler idempotent is:

async function handleCompanyUpdate(kvkNumber, data) {
  const eventId = data.eventId;

  // Controleer of we dit event al verwerkt hebben
  const processed = await db.webhookEvents.findByEventId(eventId);
  if (processed) {
    console.log(`Event ${eventId} al verwerkt, overgeslagen`);
    return;
  }

  // Verwerk de update
  await db.companies.updateByKvk(kvkNumber, {
    tradeName: data.tradeName,
    address: data.address,
    isActive: data.isActive,
    lastVerifiedAt: new Date()
  });

  // Markeer event als verwerkt
  await db.webhookEvents.create({
    eventId,
    kvkNumber,
    processedAt: new Date()
  });
}

Cache-invalidatie

Los van polling en webhooks heb je een cache-invalidatiestrategie nodig voor real-time lookups.

TTL-gebaseerde invalidatie

De eenvoudigste aanpak: stel een Time-To-Live in op je cache-entries:

const CACHE_TTL = {
  active: 24 * 60 * 60,     // 24 uur voor actieve bedrijven
  inactive: 7 * 24 * 60 * 60, // 7 dagen voor inactieve bedrijven
  notFound: 60 * 60,         // 1 uur voor niet-gevonden nummers
  search: 60 * 60            // 1 uur voor zoekresultaten
};

async function lookupWithSmartCache(kvkNumber) {
  const cached = await cache.get(`company:${kvkNumber}`);

  if (cached) {
    return cached;
  }

  const fresh = await kvkbase.lookup(kvkNumber);
  const ttl = fresh
    ? (fresh.isActive ? CACHE_TTL.active : CACHE_TTL.inactive)
    : CACHE_TTL.notFound;

  await cache.set(`company:${kvkNumber}`, fresh, ttl);
  return fresh;
}

Stale-While-Revalidate

Een geavanceerder patroon: stuur de gecachte data direct terug, maar vernieuw op de achtergrond:

async function lookupStaleWhileRevalidate(kvkNumber) {
  const cached = await cache.get(`company:${kvkNumber}`);

  if (cached) {
    // Vernieuw op de achtergrond als de data ouder is dan 12 uur
    const age = Date.now() - cached.cachedAt;
    if (age > 12 * 60 * 60 * 1000) {
      refreshInBackground(kvkNumber); // fire-and-forget
    }

    return cached.data;
  }

  // Geen cache: wacht op verse data
  const fresh = await kvkbase.lookup(kvkNumber);
  await cache.set(`company:${kvkNumber}`, {
    data: fresh,
    cachedAt: Date.now()
  });

  return fresh;
}

function refreshInBackground(kvkNumber) {
  kvkbase.lookup(kvkNumber)
    .then(fresh => cache.set(`company:${kvkNumber}`, {
      data: fresh,
      cachedAt: Date.now()
    }))
    .catch(err => console.error(`Background refresh failed: ${err.message}`));
}

Monitoring

Welke strategie je ook kiest, zet monitoring op om te weten of je data actueel is:

async function reportDataFreshness() {
  const stats = await db.companies.aggregate([
    {
      group: 'freshness',
      buckets: [
        { label: 'vandaag', where: 'last_verified_at >= NOW() - INTERVAL 1 DAY' },
        { label: 'deze_week', where: 'last_verified_at >= NOW() - INTERVAL 7 DAY' },
        { label: 'deze_maand', where: 'last_verified_at >= NOW() - INTERVAL 30 DAY' },
        { label: 'ouder', where: 'last_verified_at < NOW() - INTERVAL 30 DAY' }
      ]
    }
  ]);

  console.log('Data-versheid rapport:', stats);
}

Welke strategie kies je?

FactorPollingWebhooks
ComplexiteitLaagMiddel
Real-timeNee (vertraagd)Ja
Kosten (API-calls)HogerLager
BetrouwbaarheidHoog (jij hebt controle)Afhankelijk van provider
Beste voorKleine datasets, nachtelijke syncGrote datasets, real-time vereisten

In de praktijk werkt een combinatie het best: polling als basis met webhooks als versneller wanneer beschikbaar.

Conclusie

Het actueel houden van bedrijfsdata is net zo belangrijk als het ophalen ervan. Zonder een strategie voor updates werkt je applicatie binnen maanden met verouderde informatie.

Begin met een eenvoudige polling-strategie: een nachtelijke cron-job die bedrijven hervalideert op basis van prioriteit. Voeg webhooks toe wanneer je schaalbehoeften groeien. En implementeer altijd slimme caching met TTL-gebaseerde invalidatie voor je dagelijkse lookups.

Met KVKBase als databron kun je beide strategieen eenvoudig implementeren — de API is snel genoeg voor polling en ondersteunt de patronen die je nodig hebt voor betrouwbare bedrijfsdata.