Andros Fenollosa: How I built a WhatsApp client for Emacs without an API

Wait 5 sec.

One of the most fun things you can do with Emacs is to build an interface for another piece of software. It is the Lisp version of running Doom on any device. It is also quite useful, because you can connect it to other tools in the gnu's editor and build more pleasant workflows.The big question: can I chat on WhatsApp from Emacs? Spoiler: I made it, with some limitations.No way inContext: I work on macOS. It matters, because the solution depends on it.Meta offers no API for this, and the macOS desktop app has no bridge at all: no open ports, no gRPC, no AppleScript dictionary. Nothing.There are unofficial clients, like whatsmeow, that talk directly to WhatsApp's servers by pretending to be a device. They work, but your account ends up depending on software Meta does not authorize, and it is one more process to maintain. On top of that, Meta can detect them and ban your number. I am not going to risk my account just to chat from Emacs.So how do you talk to an app that does not want to talk to you? By reading the same thing it reads.Reading: the databaseThe WhatsApp app stores every chat in a local SQLite file:~/Library/Group Containers/group.net.whatsapp.WhatsApp.shared/ChatStorage.sqliteEmacs 29 ships with SQLite built in, no external libraries. Opening it read-only takes one line:(sqlite-open file t)The schema is undocumented, so I had to explore it by hand. Chats live in ZWACHATSESSION, messages in ZWAMESSAGE, and group member names in ZWAGROUPMEMBER and ZWAPROFILEPUSHNAME. That Z at the start of every name gives it away: it is a Core Data database.And Core Data has a surprise. Dates do not count from 1970, but from January 1, 2001. My first messages showed up dated 1995. The fix: a constant.(defconst whatsapp--epoch-offset 978307200 "Seconds between the Unix epoch and the Core Data epoch (2001-01-01).")There were more traps. Pinned chats store a date in the future so they stay on top, and the text of the session's last message is encoded. Better to read both from the message itself.One more detail: macOS protects that directory. You have to give Emacs Full Disk Access in the privacy settings. I would hand Emacs the keys to my house if it asked.Sending: the Shortcuts trickReading is easy. What about writing? Editing the database is pointless: the app would not notice and nothing would go out over the network.Then I remembered that the WhatsApp app provides an action in macOS Shortcuts: "Send Message". And Shortcuts can be run from the terminal:shortcuts run "Send WhatsApp" --input-path message.jsonThe problem is that the action expects a contact from your address book, not a number. The trick: build a vCard on the fly with the chat's phone number, which the shortcut turns into a temporary contact. Nothing is saved in your address book.Emacs writes a JSON file with the vCard and the text, starts the process and waits:(json-encode `((vcard . ,(whatsapp--vcard name phone)) (text . ,text)))The full flow looks like this:flowchart LR E[Emacs] -- reads --> DB[(ChatStorage.sqlite)] E -- JSON with vCard --> S[Send WhatsApp shortcut] S -- Send Message --> W[Official WhatsApp app] W -- stores --> DBI like one detail. While the shortcut runs, the message shows as (sending...). When WhatsApp stores it in its database, Emacs replaces it with the real one. What you see is what WhatsApp actually sent, not what Emacs thinks it sent.The resultAn ERC or rcirc style interface, with a chat list and one buffer per conversation:--- Wednesday 1 October 2026 ---[08:15] Good morning![08:16] Morning! Are we still on for today?[08:20] Yes, 10:00 at the station[08:21] [image] This is the entranceAlice> _This is the most trivial part of the whole thing. Emacs solved this problem decades ago.The whole client fits in a few lines of Elisp, with no dependencies.It has its limits, of course.It only works on macOS.You cannot write to groups, because the Shortcuts action only accepts people.Some chats do not store the phone number locally and stay read-only.And there are no notifications: you press C-c C-u to fetch new messages.But now I can talk to my family without leaving Emacs. What more could you ask for?Sourceswhatsapp.el, the code and the instructions to create the shortcut.SQLite in Emacs, native support since Emacs 29.Shortcuts from the command line, Apple's documentation for the shortcuts command.vCard 3.0 (RFC 2426), the contact format the trick relies on.Help me keep writing Every coffee gives me a push toward the next article. Sure, it's on me!Send an email to comment+article-782e8f3b@andros.dev to leave a comment. The subject will be ignored.