← Blog

Voice agents

Voice-agent case study: tandartsrecalls in NL en Arabisch

Een tandartsgroep in Den Bosch moest 1.820 patiënten per week bellen in Nederlands en Arabisch zonder één BSN bloot te stellen. Dit is de architectuur die we bouwden.

Jacob Molkenboer· Oprichter · A Brand New Company· 12 jun 2026· 10 min
Crème bakelieten telefoon op donkergroene leren onderlegger, groen lint op haak, ivoren kaart, koperen bel, rode lakzegel.

De recall-lijst die niemand wilde

Het was een vrijdag in februari. De praktijkmanager van een tandartsgroep met vier vestigingen in Den Bosch printte haar wekelijkse recall-lijst om 16:00, liep ermee naar de balie en keek naar de gezichten van de assistentes. De lijst was 1.800 patiënten lang. Gebitsreinigingen, halfjaarlijkse controles, orthodontische follow-ups, prothesepassingen. Elk een telefoontje. Elk gesprek dertig seconden praten en twee minuten typen in Exquise. Ze had budget voor een voice-agent. Ze had nog geen manier om er één op haar patiëntendatabase los te laten zonder een BSN-lek te riskeren.

Ze had het al doorgerekend. Bij tweeënhalve minuut per gesprek vrat de lijst veertig uur per week aan de vier balies. De helft van de gesprekken ging naar voicemail. Een kwart van de patiënten was verhuisd, gewisseld van zorgverzekeraar of sprak thuis geen vloeiend Nederlands. De assistentes haatten het. De tandartsen klaagden dat de stoel op dinsdag negentig minuten leeg stond omdat niemand had bevestigd.

Dat is het project dat we oppakten.

Exquise, ODBC en een 16 jaar oud schema

Het eerste gesprek met de IT-lead was twintig minuten context. Het patiëntsysteem is Exquise, een Nederlands tandartsenpakket dat bestaat sinds eind jaren negentig. Hun installatie ging live in 2009 op een Windows Server met een SQL Server-backend. Zestien jaar patiëntenhistorie, afsprakennotities, behandelplannen, declaraties. Het onderhoudscontract met de leverancier loopt, maar het API-oppervlak is beperkt en het schema is, eerlijk gezegd, organisch gegroeid.

We deden het enige zinvolle. We vroegen niet om een nieuwe API. We vroegen om een read-only ODBC-verbinding.

ODBC is ouder dan de meeste mensen op dit project. Het is ook saai, goed begrepen en ondersteund door elke database die ertoe doet. Met een read-only DSN op de Exquise SQL Server konden we de recall-lijst opvragen, de voorkeurstaal van de patiënt, het telefoonnummer, de afsprakenhistorie en de toestemmingsvlaggen. We konden geen enkele rij wijzigen. De DBA kon het account in één statement intrekken.

Read-only was om twee redenen niet onderhandelbaar. Het supportcontract van de leverancier vervalt zodra een derde partij in de database schrijft. En de praktijkmanager wilde een kill switch die niet afhing van of wij wakker waren.

Zo zag de uiteindelijke connection string eruit, zonder de credentials.

[exquise_recall_ro]
Driver = ODBC Driver 18 for SQL Server
Server = tcp:exq-sql.internal,1433
Database = ExquiseProd
UID = svc_recall_ro
Encrypt = yes
TrustServerCertificate = no
ApplicationIntent = ReadOnly

De ApplicationIntent=ReadOnly-vlag op een SQL Server AlwaysOn-replica was waar de DBA van ontspande. We zaten niet eens op de primary node.

De redactie-laag die het BSN ziet zodat het model dat nooit doet

Dit is het deel dat het langst duurde in ontwerp en het kortst in code.

In Nederland draagt elk patiëntdossier een BSN, het Burgerservicenummer. Het is dé identifier die een persoon koppelt aan zijn zorgverzekering, belastinggegevens en GBA-registratie. Hem terloops delen is niet alleen onprofessioneel. De Autoriteit Persoonsgegevens behandelt vermijdbare BSN-blootstelling als een meldplichtig datalek onder de AVG.

De opdracht was helder. De voice-agent mag nooit een BSN zien, er nooit één loggen, er nooit één in een prompt zetten en er nooit één doorsturen naar een modelprovider. Niet om performance, niet om juridische dekking. Voor de patiënt.

De architectuur die we uiteindelijk bouwden heeft drie lagen.

  1. Een query worker op de eigen infrastructuur van de tandartsgroep die de read-only ODBC-query draait en per patiënt een JSON-record produceert.
  2. Een redactie-proxy die het BSN uit het record haalt vóór het de firewall van het pand passeert, en vervangt door een korte opaque token die we per gesprek genereren.
  3. De voice-agent, gehost aan onze kant, die alleen de opaque token, de voornaam van de patiënt, het aangeboden tijdslot en een taalhint te zien krijgt.

Wanneer de patiënt een slot bevestigt, stuurt de agent een record terug met de opaque token als sleutel. De redactie-proxy resolvet de token lokaal terug naar het BSN en schrijft de bevestiging in het boekingssysteem via een apart, smal afgekaderd write-pad (daarover later meer). De modelprovider krijgt de geresolvede waarde nooit te zien.

# redactor, runs on-prem
def redact(patient_record):
    token = secrets.token_urlsafe(12)
    BSN_VAULT[token] = patient_record["bsn"]   # stays on this box
    return {
        "patient_token": token,
        "first_name": patient_record["voornaam"],
        "language_hint": patient_record["taal"] or "nl",
        "last_visit": patient_record["laatste_bezoek"],
        "recall_reason": patient_record["recall_type"],
        "offered_slots": next_open_slots(patient_record["voorkeur_arts"]),
    }

Die BSN_VAULT is een Redis-instance op de on-prem machine met een TTL van 24 uur op elke token. Na 24 uur wordt de patiënt opnieuw aangeroepen of helemaal niet. De vault heeft geen uitgaande netwerkroute.

Kort gezegd

Als een model een identifier nooit ziet, kan geen enkele beleidswijziging van de provider hem ooit blootleggen. Redactie aan de grens wint van vertrouwen in een contract.

We hebben deze aanpak niet uitgevonden. Het is ruwweg dezelfde vorm als een tokenization vault voor betalingen. Wat nieuw is, is de urgentie. Modelproviders passen retentiebeleid aan op hun eigen tempo, niet het jouwe. Een praktijkmanager die dinsdagochtend een nieuwsbericht leest over een logretentie van dertig dagen wil woensdag niet aan haar FG zitten uitleggen wat er aan de hand is. Zelfs als je de provider vertrouwt, mag je die beslissing niet eenzijdig nemen voor 14.000 patiënten. Beter om te zorgen dat de vraag nooit aan de orde komt.

Nederlands eerst, Arabisch tweede, code-switching ertussenin

Ongeveer 18% van het patiëntenbestand van deze praktijk spreekt thuis Arabisch, vooral Levantijnse en Marokkaanse dialecten. De assistentes hadden al lang geen vast script meer. Ze openden in het Nederlands, kregen een aarzelend antwoord en schakelden over. Soms midden in een zin. Soms nam het volwassen kind van de patiënt de telefoon over.

We konden niet doen alsof dat een schoon twee-talen-probleem was.

Wat we hebben opgeleverd is een voice-agent die opent in de geregistreerde taalvoorkeur van de patiënt, naar de eerste drie seconden antwoord luistert en doorroutet naar de andere taalstack als de gesproken taal niet matcht. De detectie is bewust goedkoop en bewust gebiased richting Nederlands. False positives richting Arabisch waren slechter voor het vertrouwen dan false positives richting Nederlands.

Het mechanisme is een kleine voice-activity gate gevolgd door één korte clip naar een meertalige classifier. De classifier geeft een Nederlands-of-Arabisch label terug en een confidence score. Onder 0,7 confidence blijft de agent op Nederlands en wacht op een tweede beurt. De eerste versie van deze laag had vier zelfgemaakte heuristieken achter de classifier gehangen en was minder accuraat dan de classifier alleen. We hebben ze geschrapt.

Voor Arabisch gaat het voice-model netjes om met Modern Standaardarabisch en struikelt het beleefd over dialect. We hebben dat niet proberen te repareren. De fallback is eerlijk. De agent zegt, in het Nederlands en daarna in het Arabisch, dat het een geautomatiseerde assistent is en biedt aan een terugbelafspraak in te plannen met een menselijke assistente als de patiënt dat liever heeft. Ruwweg 4% van de Arabisch-talige gesprekken neemt die uitgang en we beschouwen dat als een feature.

Een stille ontwerpkeuze. De agent gebruikt nooit de achternaam van de patiënt. Uitspraak is lastig, verkeerde uitspraak is onbeleefd, en de voornaam plus een bevestigingsvraag is genoeg om de identiteit te verifiëren samen met de geboortedatum die de patiënt opgeeft.

Bevestigingen terugschrijven zonder schrijfrechten

Het leverancierscontract verbiedt schrijfacties door derden. De praktijk heeft toch bevestigde afspraken nodig in Exquise zonder dat een mens iets overtypt. Deze twee feiten moesten naast elkaar bestaan.

Het antwoord was een kleine on-prem service die we schreven, in beheer bij de IT-lead van de tandartsgroep, met één HTTP-endpoint: POST /confirm. Hij accepteert een payload die door de redactie-proxy is ondertekend. Hij valideert de handtekening, resolvet de token lokaal terug naar een BSN en roept dezelfde officiële Exquise-boekingsmodule aan als de desktopclient van de assistentes, via het gedocumenteerde integratie-oppervlak van de leverancier. We schrijven niet naar de database. We bedienen de ondersteunde boekingsflow met het geresolvede patiëntdossier.

Als de boekingsaanroep faalt, schrijft de service een rij in een lokale SQLite-queue en stuurt een webhook naar het Teams-kanaal van de praktijkmanager. Storingen zijn zeldzaam maar luid.

POST /confirm HTTP/1.1
Content-Type: application/json
X-Signature: ed25519:...

{
  "patient_token": "Hk2v7Tg5pQ8x",
  "slot_id": "2026-06-18T10:20+02:00:arts_3",
  "language_used": "ar",
  "agent_run_id": "vra_01HZJ7Q5",
  "confirmed_at": "2026-06-12T11:42:18+02:00"
}

De agent_run_id is het enige stukje voice-agent state dat we aan onze kant bewaren, en hij is alleen gekoppeld aan de opaque token. Komt er een week later een klacht van een patiënt, dan geeft de praktijkmanager ons de run id en kunnen we de gespreksaudio ophalen (aan onze kant opgeslagen, versleuteld, 30 dagen retentie) zonder die ooit zelf naar een BSN te hoeven herleiden. De audit chain aan de praktijkkant is het omgekeerde: hun boekingslog houdt het BSN en de run id, onze log houdt de run id en de audio, en de twee ontmoeten elkaar alleen binnen de redactie-proxy onder een ondertekende lookup.

Hoe 1.820 wekelijkse belrondes er in de praktijk uitzien

De agent draait sinds eind januari. Het ritme is gestabiliseerd op ongeveer 1.820 uitgaande gesprekken per week over vier vestigingen. Ongeveer 71% verbindt bij de eerste poging. Daarvan bevestigt 58% een slot in hetzelfde gesprek. De rest plant opnieuw in, vraagt een terugbelafspraak met een mens of zegt de recall beleefd af. De agent geeft het op na drie pogingen verspreid over twee dagen en schrijft de patiënt terug naar de menselijke wachtrij. Niemand wordt elf keer gebeld.

De balies besteden hun recall-uren nu aan de exception queue. De exception queue is de enige plek waar een menselijke stem nog nodig is: klachten, complexe herplanningen, patiënten die specifiek om een persoon hebben gevraagd, en de 4% Arabisch-talige fallback. De praktijkmanager vertelde ons dat de balies rustiger aanvoelen, ook al ligt de doorzet hoger.

De cijfers die we wekelijks bekijken:

  • Connect rate, nu 71%, was 64% voordat we het belvenster zo afstelden dat we 12:00 tot 13:30 oversloegen.
  • Bevestigingen bij het eerste gesprek, 58%.
  • Gevulde slots per stoel per week, 14% hoger dan dezelfde maand vorig jaar, al is een deel daarvan seizoensgebonden.
  • Klachten per duizend gesprekken, drie, allemaal door mensen op te lossen, geen enkele over identiteitsblootstelling.

We publiceren de kosten per gesprek niet extern. Intern liggen ze ruwweg een ordegrootte onder de volledige kosten van een baliemnuut, en dat is wat we nodig hadden voor de business case.

Wat we de volgende keer anders zouden bouwen

Twee dingen.

Het eerste is de taaldetectie-stack. We hebben hem in de eerste sprint te ingewikkeld gemaakt en moesten de helft weer slopen. Een goedkope voice-activity detector plus een korte clip naar een meertalige classifier was accurater dan de geketende heuristieken die we probeerden te bouwen. De volgende keer beginnen we goedkoop, meten we een week en voegen we pas lagen toe als de confusion matrix daarom vraagt.

Het tweede is het toestemmingsoppervlak. We hebben pas laat in het project een opt-out via SMS toegevoegd omdat twee patiënten erom vroegen. Dat had in de eerste sprint moeten zitten. Voice-agents op deze schaal hebben vanaf minuut één een escape hatch van één regel nodig, niet na de eerste klacht. De SMS-lijn fungeert nu ook als correctiekanaal voor taalvoorkeur, wat we niet hadden voorzien en wat in week twee ongeveer een derde van de taal-mismatch-incidenten wegnam.

Let op

Heeft je voice-agent vanaf het eerste gesprek geen SMS- of web-opt-out, dan bouw je dat oppervlak later onder druk alsnog. Doe het op dag één.

De saaie grens is het interessante deel

Het interessante werk aan voice-agents dit jaar zit niet in het voice-model. Het zit in de saaie grens tussen het model en de systemen die het bedrijf al draaien. Read-only ODBC, een opaque token vault, een ondertekend write-endpoint in een gedocumenteerd integratie-oppervlak. Niks ervan is nieuw. Alles bij elkaar maakt het een 16 jaar oud patiëntsysteem veilig genoeg om er een voice-agent op te zetten.

Het patroon reist mee. Vervang Exquise door een zestien jaar oude logistieke planner, een groothandel-ERP of een Drupal 7-ledendatabase, en dezelfde drie stukken blijven gelden. Een read-only deur naar het systeem van record. Een grens die de identifier strippt die het model niets aangaat. Een ondertekend, smal write-pad terug via welk interface de leverancier ook nog op papier ondersteunt. Het voice-model in het midden wordt het makkelijke deel.

Toen we deze voice-agent bouwden voor de groep in Den Bosch, liepen we tegen die read-only contractclausule met de leverancier aan. We hebben het opgelost door de eigen boekingsflow van de leverancier aan te roepen vanuit een on-prem service die de praktijk zelf bezit, zodat de garanties op database-integriteit blijven liggen bij de mensen die ervoor getekend hebben.

Heb je een recall-lijst, een verouderd systeem en een BSN-vormig probleem, dan is het kleinste wat je vandaag kunt doen: open de database-handleiding en check of je DBA jou een read-only ODBC-gebruiker kan geven. De rest komt daarna.

Kern

Als het model de identifier nooit ziet, kan geen enkele beleidswijziging van de provider hem ooit blootleggen. Redact aan de grens, vertrouw niet op het contract.

FAQ

Waarom ODBC in plaats van een moderne REST API?

Het API-oppervlak van de leverancier voor deze installatie uit 2009 is beperkt, en een read-only ODBC DSN was de snelste weg naar veilige, intrekbare toegang zonder het supportcontract te schenden.

Hoe houd je het BSN weg bij de modelprovider?

Een on-prem redactie-proxy haalt het BSN eruit voor enig record de firewall passeert, en wisselt het in voor een korte opaque token. De token resolvet lokaal terug naar het BSN, nooit in het model.

Wat gebeurt er als een patiënt liever een mens spreekt?

De agent biedt in het Nederlands en Arabisch een pad naar een menselijke terugbelafspraak. Ongeveer 4% van de Arabisch-talige gesprekken neemt die uitgang, en ze landen in dezelfde exception queue als klachten en complexe herplanningen.

Werkt dit ook voor niet-tandartsenpraktijken?

Ja. De vorm (read-only ODBC, on-prem token vault, ondertekend write-endpoint in een gedocumenteerde boekingsflow) is overdraagbaar naar elk verouderd systeem met patiënt- of klantidentifiers die het gebouw nooit mogen verlaten.

voice agentscase studyintegrationsoperationsarchitecture

Iets bouwen?

Start een project