← Blog

Voice agents

Voice agent triage in 90 seconden: een Gentse case study

Hoe een Gentse vastgoedbeheerder van 19 man wekelijks 1.340 huurdersgesprekken routeert via een voice agent — en een gaslek binnen 90 seconden in een monteurwachtrij parkeert.

Jacob Molkenboer· Oprichter · A Brand New Company· 18 jun 2026· 9 min
Zwarte bakelieten telefoonhoorn naast haak op ivoren vloeipapier, groen lint op vork, koperen bel, kaart met lakzegel.

Het is vrijdagavond 23:14 in Ledeberg. Een huurster aan de Hundelgemsesteenweg staat op haar keukenblad, met een badhanddoek tegen het plafond gedrukt. Door het lichtarmatuur stroomt water naar binnen. Ze belt haar vastgoedbeheerder. Het kantoor sloot om 18:00. Het wachtnummer ging vroeger naar voicemail, en als ze geluk had, daarna naar de persoonlijke gsm van een junior accountmanager.

Dat was de situatie waar wij vorig september instapten, toen een vastgoedbeheerder van 19 man in Gent — laten we ze VBR Gent noemen — ons vroeg de buiten-kantoorureninstroom op te lossen. Ze beheren zo'n 4.800 eenheden in Oost-Vlaanderen. Volgens hun eigen tellingen kwamen er wekelijks 1.340 huurdersmeldingen binnen, en zo'n 18% daarvan buiten kantooruren. De vorige flow was: voicemail, triagesheet op maandagochtend, ticket. De 24-uurs SLA-klok uit de Vlaamse Wooncode was al voor twee derde verstreken tegen de tijd dat iemand het ticket las.

Dit is het verhaal van de voice agent die we tussen de huurder en de bestaande systemen plaatsten, en wat we leerden door AI vast te lijmen aan een dertien jaar oud document management systeem dat nog steeds SOAP-over-XML uitspuugt.

De stack waar we instapten

De stack van VBR Gent was niet ongewoon voor een middelgrote Vlaamse vastgoedbeheerder. Drie dragende stukken:

  • Een kopie van Square — het document management systeem, niet het betalingsbedrijf — on-premises, versie 4.7, geïnstalleerd in 2013. Een SOAP API die XML teruggeeft, ingepakt in CDATA, ingepakt in JSON als je het netjes vraagt. Eenhedenregister, eigenaars, huurders, contracten en opgeslagen pdf's leven hier allemaal.
  • Een zelfgebouwd PostgreSQL onderhoudsticketsysteem, geschreven door een senior dev die inmiddels weg is. Het schema is verstandig: units, tenants, tickets, assignments, monteurs, sla_clocks. Triggers vuren op insert. Er is geen API; intern personeel werkt via een Vue 2 frontend tegen de DB door een dunne Express-laag heen.
  • Een poule van 14 externe aannemers (elektriciteit, loodgieterswerk, gas, beglazing, slotenmaker) bereikbaar via WhatsApp Business en sms. Drie ervan hebben een echte 24/7-lijn. De rest belt binnen het uur terug.

Wat we ook bouwden, het moest met alle drie kunnen praten, en we mochten de Square-server niet aanraken. De Postgres-DB mochten we uitbreiden met nieuwe tabellen en views. De aannemersbereikbaarheid mochten we vanaf nul herontwerpen.

Voice boven chat

De reflex van 2026 is: zet een chat agent op de website. We hebben het zes weken getest voordat we de aanbeveling deden. De data was helder: huurders in nood openen geen browser. Ze pakken de telefoon. Van 248 spoedmeldingen tijdens het testvenster kwamen er elf binnen via chat. De rest kwam binnen via spraak.

WhatsApp was de volgende kandidaat. Het is het juiste kanaal voor veel vastgoedwerk — bevestigingen, foto-uploads, planning — en het bleek downstream nuttig werk te doen. Maar de inkomende kant van een noodgeval is voice, want voice is waar mensen instinctief naar grijpen als er water op hun vloer staat. Dus zowel het hoofdnummer als de fallback uit de voicemail van de front office routeren naar de voice agent. De chatwidget bleef op de site staan en handelt nu niet-urgente zaken af: sleutelvervangingen, adreswijzigingen, wanneer de poetsdienst komt.

Het routingbudget van 90 seconden

De harde eis: vanaf het moment dat de huurder zegt "ik heb een lek" tot het moment dat de telefoon van een aannemer rinkelt, mag er niet meer dan 90 seconden voorbijgaan. Dat getal is niet willekeurig. Het Vlaamse kader voor private huur behandelt bepaalde gebreken — verwarming in de winter, gas, waterindringing, een falend slot van de hoofdtoegang — als dringende herstellingen, en in de praktijk betekent dat dat de beheerder 24 uur heeft om de interventie te starten of zelf de schade draagt. Als de oproep om 23:14 op vrijdag binnenkomt en triage pas op maandag plaatsvindt, zit de klok al in de waarschuwingszone.

Die 90 seconden valt in de live agent ongeveer als volgt uiteen:

  • 0–8s: begroeting in het Nederlands met een Vlaamse stem, huurderslookup op basis van caller ID.
  • 8–35s: open vraag, classifier draait op het lopende transcript terwijl de huurder nog aan het praten is.
  • 35–60s: adres hardop bevestigen, details extraheren die de huurder niet uit zichzelf gaf (gaslucht, waterbron, verwarming beschikbaar).
  • 60–90s: ticket schrijven, NOTIFY naar het dispatchkanaal, eerste uitgaande oproep naar de aannemer.

De classifier is het stuk dat de meeste teams te ver doorkoken. Wij begonnen met een multi-labelschema van 31 categorieën. Na drie weken hebben we het teruggebracht naar zeven, want het enige wat downstream de routing daadwerkelijk verandert is de triagetier en in welke aannemerspoule het ticket landt. De rest kan ingevuld worden als de monteur ter plaatse is.

Hier is de routingregel, vereenvoudigd, die draait tegen de gestructureerde output die de LLM rond de 30 seconden uitspuugt:

def route(extracted: dict) -> str:
    if extracted["hazard"] in {"gas_leak", "fire_risk"}:
        return "p0_gas"
    if extracted["hazard"] == "water_active" and extracted["source"] != "appliance":
        return "p0_water"
    if extracted["service"] == "heating" and is_winter() and extracted["heat_failed"]:
        return "p0_heat"
    if extracted["service"] == "lock" and extracted["entry_blocked"]:
        return "p1_lock"
    if extracted["severity"] >= 7:
        return "p1_other"
    return "p2_business_hours"

De p0_*-tiers gaan direct naar een monteurwachtrij. p1 wacht tot 07:00, tenzij het al overdag is. p2 wacht op het kantoor. Meer is het niet. De voice agent probeert verder nergens slim over te doen.

Praten met een dertien jaar oud DMS

Square is het onderdeel dat de meeste engineeringtijd heeft opgegeten. De SOAP-endpoints waren stabiel en voor hun tijd goed gedocumenteerd, maar de auth-flow gebruikte een per-session token dat agressief verliep, en de WSDL declareerde optionele velden die de server als verplicht behandelde. We mochten de Square-server niet wijzigen. We kregen wel een read-only mirror van drie kerntabellen — units, tenants, leases — die elke nacht naar een Postgres-schema in ons beheer werd gerepliceerd. Daarmee hadden we de lookup die we nodig hadden.

Voor writes terug naar Square (we hangen de opname en het transcript van het gesprek als document aan de unit) draait een kleine worker die één warme session vasthoudt en writes serialiseert:

class SquareWriter:
    def __init__(self, host, user, pw):
        self._client = zeep.Client(f"https://{host}/dms?wsdl")
        self._user, self._pw = user, pw
        self._token = None
        self._token_acquired = 0

    def _ensure_token(self):
        if not self._token or time.time() - self._token_acquired > 540:
            self._token = self._client.service.Login(user=self._user, password=self._pw)
            self._token_acquired = time.time()

    def attach_document(self, unit_id: str, payload: bytes, filename: str):
        self._ensure_token()
        return self._client.service.AttachDocument(
            token=self._token,
            unitId=unit_id,
            fileName=filename,
            fileBytes=base64.b64encode(payload).decode(),
        )

Niks spannends. Het bestaat omdat een session token verliezen halverwege een attach in week twee de meest voorkomende productiefout was. Writes serialiseren door één proces heeft die klasse bugs volledig de nek omgedraaid.

Waarschuwing

Als je een verouderd SOAP-DMS inpakt met een worker als deze, zet de RestartSec van die worker dan op minstens 30 seconden. We hadden een flappende crashloop die zich binnen een minuut 200 keer opnieuw inlogde en de brute-force-lockout van het DMS een uur lang activeerde. De read-only mirror is wat ons dat uur gered heeft.

NOTIFY verslaat een poll loop

Het zelfgebouwde ticketsysteem is de bron van waarheid voor wie waaraan werkt. We voegden twee tabellen toe (voice_calls, voice_call_events) en één view (open_tickets_for_dispatch). Wanneer de voice agent een gesprek afsluit, doet hij een insert van het ticket, een insert van een event en stuurt een Postgres NOTIFY op het dispatch-kanaal.

Een kleine Go-dispatcher LISTEN't op dat kanaal. Wanneer een NOTIFY binnenkomt, haalt hij de rij op, zoekt de oproepbare aannemer voor de juiste tier op, en plaatst óf een uitgaand gesprek via Twilio óf stuurt een WhatsApp-templatebericht, afhankelijk van de voorkeur van de aannemer.

CREATE OR REPLACE FUNCTION notify_dispatch() RETURNS trigger AS $
BEGIN
  IF NEW.priority IN ('p0_gas', 'p0_water', 'p0_heat', 'p1_lock') THEN
    PERFORM pg_notify('dispatch', NEW.id::text);
  END IF;
  RETURN NEW;
END;
$ LANGUAGE plpgsql;

CREATE TRIGGER trg_notify_dispatch
  AFTER INSERT ON tickets
  FOR EACH ROW EXECUTE FUNCTION notify_dispatch();

De eerste maand pollden we elke 5 seconden. Overschakelen naar LISTEN/NOTIFY bracht de mediane dispatchlatentie omlaag van 4,2 seconden naar 180 milliseconden en stopte met de unit-lookup queries onnodig te belasten met achtergrondverkeer.

De SLA-klok, in code

Elk spoedticket draagt een sla_clock_started_at-kolom en een afgeleide sla_due_at = sla_clock_started_at + interval '24 hours'. De dispatcher weigert een ticket op in behandeling te zetten tot een aannemer expliciet de pick-up heeft bevestigd. Gebeurt dat niet binnen tien minuten, dan escaleert de dispatcher naar de volgende aannemer in de rota. Is de rota uitgeput, dan rinkelt de telefoon van de kantoormanager. Dat is in acht maanden twee keer gebeurd. Beide keren op een zondag in februari.

De klok is tijdens het gesprek zichtbaar voor de voice agent, wat minder uitmaakt dan je zou denken — de agent gaat niet met de huurder onderhandelen over timing. Maar het kantoor ziet hem de volgende ochtend op de wallboard, en dat heeft de relatie van het team met de buiten-kantoorurenstapel veranderd. Ze komen op maandag binnen en de spoedklussen zijn al gesloten.

Wat we de eerste keer fout deden

Twee fouten die het noemen waard zijn.

Ten eerste: we lieten de voice agent unitnummers bevestigen door ze terug te lezen naar de huurder. In Gent komen unit-aanduidingen als 2B, 2bis en 2/B allemaal voor in het wild, en de Square-DB slaat ze inconsistent op. De agent zei dan vol overtuiging "unit twee-bee", de huurder zei "ja" en wij schreven een ticket tegen de verkeerde unit. We hebben het opgelost door over te schakelen naar adres-eerst-bevestiging ("Hundelgemsesteenweg 184, derde verdieping, de deur links als je de trap opkomt — klopt dat?") en van daaruit de unit op te zoeken. De accuratesse richting huurders ging van 91% naar 99,4%.

Ten tweede: we onderschatten de aannemerskant. De eerste versie van de dispatcher belde uit via Twilio en las het ticket voor met een synthetische stem. Drie aannemers weigerden botweg om 2 uur 's nachts robocalls aan te nemen, wat terecht is. We zijn overgestapt naar WhatsApp-templates met een audiobijlage van de stem van de huurder zelf die het probleem beschrijft. De pickup-tijd op p0-tickets daalde van een mediaan van 6 minuten naar 90 seconden. De aannemers zeiden dat de stem van de huurder zelf hun in vijftien seconden meer vertelde dan welk gestructureerd formulier dan ook.

De cijfers na zes maanden

  • 1.340 gesprekken per week gemiddeld; 1.418 in de drukste week (koudegolf, januari).
  • 96,1% van de p0-tickets binnen 90 seconden end-to-end naar een aannemer gedispatcht.
  • Mediane gespreksduur huurder: 2 minuten 41 seconden.
  • Twee SLA-overtredingen in acht maanden, beide te wijten aan een uitgeputte aannemersrota.
  • Telefooninterrupties van de kantoormanager buiten kantooruren: van ~14 per week naar 0,3 per week.

Het cijfer van 14 naar 0,3 is wat de oprichter erover vertelt. De taak van de voice agent is niet om indruk te maken in een demo. Het is om een team van 19 man zich te laten gedragen als een team van 40, zonder dat iemand zijn zondagen kwijt is.

Wat je morgenochtend kunt doen

Zit je op een vergelijkbare stapel — verouderd DMS, zelfgebouwde ticket-DB, aannemerspoule op WhatsApp — dan is de kleinste nuttige audit deze week twee dingen tellen. Hoeveel spoedmeldingen komen er buiten kantooruren binnen, en wat is de mediane wachttijd voordat een mens er één bekijkt. Is dat tweede getal meer dan twee uur, dan heb je een voice-probleem, geen chatbot-probleem.

Toen we dit voor VBR Gent bouwden, was waar we steeds tegenaan liepen dat de waarde niet in het model zat — die zat in de bedrading tussen drie systemen die nooit bedoeld waren om met elkaar te praten. Dat is het meeste werk in elke productie-voice agent: de saaie verbindingen betrouwbaar genoeg maken zodat het slimme deel simpel kan blijven.

Kern

De waarde van een productie-voice agent zit niet in het model — ze zit in de bedrading tussen de systemen die nooit ontworpen zijn om met elkaar te praten.

FAQ

Waarom een voice agent voor huurders in plaats van een chatbot?

Huurders in nood pakken een telefoon, geen browser. In een test van zes weken kwamen er van de 248 spoedmeldingen er maar 11 binnen via chat. De rest waren oproepen.

Hoe gaat het systeem om met de 24-uurs SLA uit de Vlaamse Wooncode?

Elk spoedticket draagt een SLA-deadlinestempel. Bevestigt een aannemer niet binnen tien minuten de pick-up, dan escaleert de rota automatisch, en bij een uitgeputte rota wordt de kantoormanager gebeld.

Accepteerden de aannemers dat een AI ze dispatcht?

In eerste instantie niet. Robocalls om 2 uur 's nachts waren voor drie van hen een no-go. WhatsApp-templates met een audioclip van de stem van de huurder zelf brachten de mediane pickup-tijd terug van 6 minuten naar 90 seconden.

Hoe accuraat is de unitherkenning op Vlaamse adressen?

Nadat we stopten met unitnummers terug te lezen naar huurders en overschakelden op adres-eerst-bevestiging, ging de accuratesse van 91% naar 99,4%. De nummering in Gent is te inconsistent om het op een andere manier vast te krijgen.

voice agentscase studyintegrationsautomationlegacy sitesoperations

Iets bouwen?

Start een project