If your product connects to WhatsApp through the linked-device protocol, your code operates a phone number that belongs to someone else. A small business owner scans a QR code and hands you their working line, the one printed on their van and saved in every customer's contacts.
The cost of a mistake is not a 500 in your dashboard. It is a ban, and a ban is not something you can refund. Only the messenger lifts it, on the owner's appeal, on their timeline.
What surprises most teams is that the anti-fraud system does not only read outgoing text. It watches how your protocol client behaves. A number can be flagged after two messages in a week if the client around it looks wrong.
Below are four invariants I now treat as non-negotiable for any system of this kind. None of them are about content.
1. Reaction is allowed, initiative is not
Replying to an incoming message is fine. Everything your process starts on its own is suspect.
Never let automation tear down a live session, request a fresh QR, wipe stored credentials, or scan the server for numbers on a schedule. Those actions belong to an explicit human click in your UI, and nowhere else.
The dangerous code is always well-intentioned. It reads like this:
// looks caring, behaves like an unstable client
if (Date.now() - lastInbound > SILENCE_THRESHOLD) {
await restartSession(sessionId);
}
The intent is "the channel has been quiet, let me reconnect just in case". From the outside, a client that re-authenticates a healthy session three times in a morning is indistinguishable from a compromised or emulated one. Silence is usually just a quiet Tuesday.
If you need a liveness check, make it one auth-safe probe per silence episode with a long cooldown, and log the outcome. Anything that can loop must not touch authentication.
2. Any bulk lookup must converge
Sooner or later you will want to resolve identifiers: map contacts, check which numbers exist, backfill a table after a protocol change. That loop talks to the messenger about people who are not your users.
Three properties make it acceptable:
- it remembers negative answers, not only successful ones
- it has a ceiling per pass
- it has a minimum interval between passes
Remembering only successes is the classic bug. Every pass re-asks about the same unresolvable numbers, the query count never drops, and the loop runs forever at full volume.
The convergence probe is arithmetic. Count the lookups per day and compare consecutive days. Falling is healthy. Flat means the loop is asking the same questions forever, and from the provider's side a permanent high-volume stream of questions about third-party numbers looks exactly like scraping a contact database. That is a category they ban for on its own, no matter how few messages you send.
// the fix is one table, not a rewrite
await recordLookupResult(number, result); // including "not found"
const pending = await pickPending({ limit: CEILING, olderThan: MIN_INTERVAL });
3. A process restart is not a reason to touch every session
Deploys are when fleets die together. The container comes up, the boot path iterates over stored sessions, and every number reconnects inside the same second.
Stagger it. A dozen seconds between reconnects plus jitter, and no automatic connection at all for sessions with no stored credentials, since those can only produce a QR request that nobody asked for.
for (const [i, session] of sessions.entries()) {
if (!session.hasStoredAuth) continue; // QR is a human decision
await sleep(i * 12_000 + Math.random() * 4_000);
void connect(session);
}
4. There are no silent touches
Every disconnect, reconnect and re-auth gets a row in a durable log with a parsed reason, and that log has to outlive the process that writes it.
This sounds like ordinary hygiene until the day you need it. When a number gets flagged, you have one question to answer: what did our client do in the hours before. If your telemetry lived in container stdout, the answer is a guess, and guesses are how teams end up blaming the messenger for their own restart loop.
Write the events to a table. Mount the raw log on a volume. Archive it on rebuild. It costs nothing and it is the difference between a post-mortem and a shrug.
The test to run on your own code
Take any automation you have that touches a session, and ask: if this misfires ten times in a row on a live customer number, what does the provider see?
Ten replies to ten incoming messages, fine. Ten re-authentications, ten QR requests, or ten thousand queries about strangers' numbers, not fine. That question catches the whole class, and it catches it before someone loses their business line rather than after.
I build DOS AI, where these rules are enforced by the platform rather than left to the integrator. Happy to compare notes in the comments if you run a fleet of your own.
Top comments (0)