Security
WhatsApp Business agents: a BSN-safe audit checklist
A Dutch DPO replied to our Friday demo with one line: every WhatsApp Cloud API payload sits on a US server for 30 days. Here is the audit we now run before any agent goes live.

The Friday email that killed our launch
It is 16:48 on a Friday in May. A WhatsApp agent for a Dutch healthcare administrator in Utrecht is scheduled to go live on Monday morning. The intake flow is wired up, the knowledge base is loaded, the escalation logic is tested against twenty staged conversations. The client's project lead is happy. Then her DPO replies to the demo thread with a single paragraph.
"You realise that every inbound message we just sent through the Cloud API is now sitting on a Meta server in the United States for the next thirty days, right? Including the BSN that our test patient typed in during the booking flow."
She is right. We had run the agent through a clean staging tenant, scrubbed the prompt templates, set up our own database in eu-central-1, and built a careful audit log on our side. None of that mattered. The WhatsApp Cloud API itself logs every payload (the message, the media URLs, the contact metadata) and retains it for up to 30 days in Meta's US-region storage. Meta's own documentation is clear about this if you read the data-handling pages carefully. The DPO had.
The launch slipped two weeks. What follows is the checklist we now run before any chat agent in our shop is allowed to answer a question that could touch a BSN, an AGB-code, or a BIG-registratie. It is dry on purpose. The interesting work happens after the checklist passes.
The 30-day log inside Meta
The Cloud API is the version of WhatsApp Business that Meta hosts for you. Most SDKs, examples, and integration guides written after late 2024 assume you are on it, because the On-Premises API was deprecated in October 2025. The trade-off is that the message broker, the queue, and the temporary storage all live inside Meta's infrastructure, not yours.
Three things happen to every inbound message before your webhook ever fires:
- The payload is decrypted at Meta's edge so it can be routed.
- A copy is written to a request log used for delivery confirmation, retries, and abuse detection.
- The decrypted body is retained for up to 30 days for operational purposes, in a region Meta picks based on routing rules. For most of our clients that region resolves to us-east.
For most B2B chat that is genuinely fine. A delivery confirmation, a shipping question, a sales lead with a name and an email address: ordinary personal data with a clear legal basis under contract or legitimate interest. For chat that quotes a citizen service number, a healthcare-provider code, or a regulated practitioner ID, it is a transfer of special-category personal data to a third country under Article 9 GDPR. Schrems II did not get easier in 2025. The EU-US Data Privacy Framework gives you a legal basis but it does not give you a clean audit answer when the Autoriteit Persoonsgegevens calls and asks where the BSN of a named patient went between 11:02 and 11:03 last Tuesday.
If your agent ever receives a free-text field that could contain a BSN, you must assume it does. Patients copy and paste from old emails. So do GPs and pharmacy staff. Hoping the user "knows not to" is not a control.
The pre-launch audit
The checklist below is what we now run before the WhatsApp business number is verified, before the webhook is pointed at production, and before the agent is shown to a real patient or a real client. Every step has a yes/no answer and a named owner. If a step is "no", the agent does not go live.
1. List every regulated identifier the agent might see
For a Dutch healthcare client that is: BSN (9 digits, validated by the 11-test), AGB-code (8 digits), BIG-registratie (11 digits), DigiD usernames, UZI numbers from the ZIN, and any combination of name plus date of birth plus postcode (which together count as identifying personal data even without a unique number). For a Dutch financial client it is: IBAN, BSN, RSIN, and KvK numbers attached to a natural person. The list goes on the wall of the project channel.
2. Decide, in writing, what the agent is allowed to do with each one
Most of the time the answer is "nothing." The agent should not echo a BSN back, should not store it in its own database, should not pass it to the LLM provider, and should not include it in any analytics event. The decision matrix lives in a one-page document the DPO signs before integration starts. Without that document, the engineering team is making policy decisions, which is the wrong default.
3. Put a redactor in front of Meta, not behind it
This is the lever most teams miss. You cannot stop Meta from logging what the user sent. You can stop the user from sending it in the first place, by replacing the chat surface for those flows with a one-shot link that opens a Dutch-hosted web form. We return to the implementation below.
4. Lock down the webhook receiver
The webhook receiver is the first line of defence inside your own perimeter. It must run inside the EU (we use eu-central-1 in Frankfurt or eu-west-1 in Ireland), it must redact regulated identifiers before anything else gets written, and it must drop the raw body from any error-reporting tool before Sentry or Datadog gets a copy. Sentry's default configuration will happily ship a BSN to its US servers as part of a stack trace. Test for that explicitly with a fake payload during integration, not during the incident review.
5. Verify the LLM call path
If your agent forwards the user's message to any LLM that processes outside the EU, you are making the same cross-border transfer twice. Most providers now offer EU-region inference endpoints, but the default endpoint in the SDK is still US. Hard-code the EU endpoint, pin it in CI with a test that fails if the URL changes, and document the region for the data-processing register.
6. Cap retention on your side
Your own logs are a parallel risk. We keep WhatsApp message bodies for seven days at most in our own systems, and the BSN-class identifiers are masked before storage. Anything older lives as a non-personal counter (timestamp, conversation length, outcome bucket). Seven days is enough to debug an issue reported on a Monday. It is not enough to build a behavioural profile, which is the point.
7. Make the escalation route the default for grey areas
Every flow that could touch a regulated identifier has a hard exit to a human or to a Dutch-hosted form. The agent should know how to say "this question needs to go through our secure portal" before the user types the number. Train the prompt with examples. Test it with adversarial messages where the user volunteers the BSN unprompted.
The redaction proxy pattern
The technical move that does the most work is a small redaction proxy that sits between the WhatsApp webhook and everything else. Every inbound message hits it first. It rewrites regulated identifiers into tokens before any downstream system (LLM, logger, database, ticketing tool, error tracker) sees the body.
The minimum viable version is about thirty lines of TypeScript:
// proxy/redact.ts
const PATTERNS = {
bsn: /\b(\d{9})\b/g,
agb: /\b(\d{8})\b/g,
big: /\b(\d{11})\b/g,
iban: /\b([A-Z]{2}\d{2}[A-Z0-9]{10,30})\b/g,
};
function isValidBsn(d: string): boolean {
if (d.length !== 9) return false;
const w = [9, 8, 7, 6, 5, 4, 3, 2, -1];
const sum = d.split('').reduce((a, c, i) => a + Number(c) * w[i], 0);
return sum % 11 === 0;
}
export function redact(text: string): { clean: string; hits: string[] } {
const hits: string[] = [];
let clean = text;
clean = clean.replace(PATTERNS.bsn, (m) => {
if (!isValidBsn(m)) return m;
hits.push('bsn');
return '[BSN_REDACTED]';
});
for (const [label, re] of Object.entries(PATTERNS)) {
if (label === 'bsn') continue;
clean = clean.replace(re, () => {
hits.push(label);
return `[${label.toUpperCase()}_REDACTED]`;
});
}
return { clean, hits };
}
The 11-test check on the BSN matters. Without it, every Dutch phone number, every 9-digit invoice line, and every random tracking code gets caught and the agent becomes useless. With the check, you only catch strings that are mathematically valid BSNs. The Wbsn definition is what the validator implements; do not invent your own version.
What you do with the hits array is the second half. In our flow, any non-empty hits array forces the agent to respond with a templated message: "I notice this question contains personal data we cannot process over WhatsApp. Please open this secure form to continue: [link]". The link is a one-time URL that loads a Dutch-hosted form, completes the intake there, and writes the result to our own EU database. Meta never receives the validated BSN, and your downstream LLM never receives it either.
You cannot stop Meta from logging what arrives. You can stop the user from sending it. The redaction proxy is what buys you the second one.
Handing off to a human, or to a form
The harder design question is when to hand off to a human versus when to hand off to a form. The right answer depends on what the user is trying to do.
For "I want to book an intake," the agent collects everything that is not a regulated identifier (the reason for the visit, the preferred date, the GP referral status) and then hands off to a Dutch-hosted booking form that asks for the BSN as the very last step. The form's submit posts to our EU database, the database notifies the practice, and the patient receives a WhatsApp confirmation from the agent that does not echo the number.
For "what was my AGB-code again, I lost the email," the agent does not try. It hands off to a human, full stop, because that is an identity-verification question dressed as a lookup. We learnt that one the hard way during a pilot in 2025 when a polite agent helpfully read back the code to someone who turned out not to be the practitioner.
For "is the BIG-registratie of Dr X still valid," there is a public registry (the BIG-register) and the agent can use a name search to confirm a public record without ever asking for the number. Lean on public data when it exists. The cleanest answer is the one that does not need a regulated identifier in the first place.
The proactive-agent problem
The trend on the AI side this month is agents getting more proactive. The Hacker News front page on the morning we wrote this had three separate threads about agents acting beyond their instructions, including one that managed to spend its operator into the ground by trying to be helpful overnight. That is exactly the failure mode this audit is built to survive. A proactive agent that decides, on its own, to "confirm receipt" by quoting the patient's BSN back into the chat is a GDPR breach with a public chatlog. The redaction proxy and the no-echo rule mean the agent literally cannot make that mistake, regardless of which model is running it. Boring guards beat clever prompts.
The five-minute audit you can do today
Pick your most exposed chat surface. Open the webhook handler in your editor. Search the source for the strings BSN, AGB, BIG, and IBAN. If the search returns zero results, your code has no idea those identifiers exist, which means nothing is masking them before they reach your logger, your LLM provider, or your error tracker. That is your starting point for Monday.
When we built the WhatsApp intake agent for a healthcare administrator in Utrecht, the off-the-shelf Cloud API integration leaked the first patient's BSN straight into our error tracker on the first failed message of staging. We solved it by putting a redaction proxy in front of every other system and routing the regulated half of the conversation to a Dutch-hosted form before any number ever reached Meta, which is the AI agents pattern we now reuse by default.
Key takeaway
You can't stop Meta from logging WhatsApp payloads. You can stop the user from sending a BSN by redacting before the proxy and routing to a Dutch-hosted form.
FAQ
Can we use the WhatsApp Cloud API for GDPR-regulated workflows at all?
Yes, if you redact regulated identifiers before the user sends them, or route those exchanges off WhatsApp entirely. The Cloud API by itself does not give you a clean audit answer for BSN-class data.
Does the EU-US Data Privacy Framework solve the BSN transfer issue?
It provides a legal basis but not an operational one. You still have to demonstrate data minimisation. The framework does not exempt you from showing why a BSN crossed a border at all.
What happened to the WhatsApp On-Premises API?
It was deprecated in October 2025. New projects must use the Cloud API or a Business Solution Provider. Neither removes your obligation to redact regulated identifiers before logging.
How long does Meta actually keep messages?
Up to 30 days in operational logs at the time of writing. Retention policies change, so re-check the developer documentation before each major launch and record the answer in your DPIA.
Can we just disable logging on our side and be done?
That closes your half of the risk. Meta's half is outside your control. Only the proxy pattern, which prevents the BSN from reaching Meta in the first place, closes both gaps.