Vishal Tyagi
← Return to Projects
Browser Extensions·production

WhatsApp Web CRM Sidecar

Capturing WhatsApp Web conversations into a CRM with no official API

Built a Manifest V3 Chrome extension that syncs WhatsApp Web conversations into a CRM by resolving the app's own internal module stores at runtime, and pages chat history without ever changing the chat a rep is looking at.

Date Shipped2026-08
Estimated Reading4 min read
Lifecycle Statusproduction
Primary Tech Stack
JavaScript, Chrome Extensions (MV3), Web Reverse Engineering

None — WhatsApp Business API does not cover reading a live personal Web session

Official API used

Named module lookup first, structural matching as fallback

Store discovery

None — active chat verified unchanged across a full history page

UI impact during sync

Milestone Timeline

  • Runtime store discovery

    Located WhatsApp Web's Chat/Msg/Contact/Conn stores inside a minified webpack bundle by structural shape matching.

  • Two-tier rewrite

    Switched to exact module-id lookup after a structural-typing false positive, keeping shape matching as a degradation path.

  • Background catch-up

    Replaced UI-driven chat switching with direct history paging against the store.

What it does

A sales rep opens WhatsApp Web, and the conversation they are already having becomes a CRM lead — messages, contact identity, and call history synced in the background, without leaving WhatsApp and without the rep clicking anything per chat.

Why it existed

The WhatsApp Business API requires a business-verified number and covers sending on a business line. It does not cover “read my own personal WhatsApp Web session, live.” That is the exact thing reps do all day, so every piece of chat and contact data had to come from reading WhatsApp Web’s own in-browser state instead. There is no supported surface for that, which sets the constraint the rest of the design answers: everything is read-path, everything can break on a WhatsApp deploy, and nothing may disturb the session the rep is working in.

Diagram
store readsruntime.onMessageper messagebatcheddeliberately absentWhatsApp Webrep's live sessionContent scriptresolve + page historyBackground workerMV3, no memorychrome.storage.localCRM APINo write pathread-only

Everything crosses that boundary in one direction. The dashed node is the property that makes this a sidecar rather than an automation bot: there is no path back into WhatsApp, not a message send and not a chat switch.

Decisions and tradeoffs

Structural matching first, then named lookup. WhatsApp Web is a minified webpack bundle with no public API. The first working approach pushed a fake chunk into WhatsApp’s own webpack runtime, force-required every already-loaded module, and kept whichever ones duck-typed correctly — an object carrying _models, modelClass, and a notSpam property is the Chat store.

That approach had a bug worth naming, because it is the kind that passes every test you think to write. The shape match was not unique: the Chat finder was resolving WhatsApp’s Newsletter collection, and the Contact finder was resolving group metadata, purely because they shared the same shape. A structural-typing false positive — right shape, wrong object.

The fix was not a better heuristic. WhatsApp Web exposes a working require(<module-id>) global directly, even before login, which resolves each store by exact id (WAWebChatCollection, WAWebConnModel) in one call with no scanning. Discovery became two tiers: named lookup first, shape matching retained only as the fallback for whatever a future WhatsApp rebuild renames. That is the difference between a hack that works today and something that degrades on a schedule you do not control.

Resolve identity through the Contact store, not the chat. WhatsApp’s newer @lid privacy identity means the “phone number” read straight off a chat is sometimes not a phone number at all. Numbers get resolved through the Contact store rather than trusted from the chat record.

Disproving the obvious hypothesis about call history. WAWebCallCollection looks correct by name and is not: it only tracks a call currently in progress, and its history array is empty the moment a call ends. The real mechanism is that the Calls tab renders ordinary chat messages with type === "call_log", already in memory for the chat list’s own preview text before the Calls tab is ever opened. Neither whatsapp-web.js nor open-wa implement an equivalent.

Never touch the driver’s seat. Earlier versions called into the UI to background-sync a chat, which visibly switched WhatsApp’s active chat mid-sync while the rep might be reading it. WAWebChatLoadMessages.loadEarlierMsgs() pages history directly against the store with zero UI interaction — verified live, with Store.Chat.getActive() identical before and after a full page — and catch-up was rewritten around it. That property is what makes it a sidecar rather than an automation bot.

Fail closed. Anything reading WhatsApp internals can be wrong after a deploy, so the sync indicator shows nothing rather than guessing. No badge is safer than a badge on the wrong contact.

Bugs closed in production logic

  • Same-second dedup. WhatsApp timestamps are per-second, so two messages can share one t. A “sync everything newer than X” check cannot distinguish already-synced from new-in-the-same-second. Fixed by tracking the specific message ids seen at the sync boundary alongside the timestamp.
  • Silent data loss in the outbox. A failed auto-sync batch was dropped the moment the next batch arrived. Failed batches now re-queue and drain on the following run.
  • A batch size that could wedge sync permanently. The bulk endpoint was assumed to accept 200 messages per call; the server caps at 50. Any account with more backlog than that would resend an oversized request that could never succeed. Fixed the constant and documented why it is not a tunable.

What it demonstrates

Reverse-engineering a moving target and then designing for the fact that it moves — including disproving my own first hypothesis rather than shipping around it. The reusable part was extracted into a standalone package with its own reverse-engineering notes, so the findings outlive the extension.

Scope, stated honestly: this is a read-path CRM sync tool. It never sends WhatsApp messages and is deliberately not a bulk-messaging or anti-ban automation product.

Architecture write-up: WhatsApp Web CRM Sidecar Architecture.