• Home
  • Blog
  • whatsmeow and Go WhatsApp Libraries: A Developer Guide
Libraries
12 min read Jun 11, 2026

whatsmeow and Go WhatsApp Libraries: A Developer Guide

How whatsmeow implements WhatsApp's multidevice protocol in Go — pairing, architecture, runnable sample code, the GOWA REST wrapper, and the honest ToS and ban-risk picture.

Key takeaways
  • whatsmeow (go.mau.fi/whatsmeow, by Tulir Asokan) is the actively maintained Go library for WhatsApp's multidevice Web protocol — it powers mautrix-whatsapp and Beeper.
  • The old Rhymen/go-whatsapp is deprecated and no longer works; its README points to whatsmeow. Don't start new projects on it.
  • Pair either with a QR code (GetQRChannel) or a phone-number pairing code (PairPhone, returned dash-formatted like ABCD-1234). Sessions persist via sqlstore (SQLite/Postgres).
  • Go gives you low memory use and goroutine concurrency for many sessions; for a truly single static binary, pair it with the pure-Go modernc.org/sqlite driver rather than CGO-based mattn/go-sqlite3.
  • whatsmeow is unofficial and reverse-engineered: it can violate WhatsApp's ToS and risks bans, even for low-volume legit use. For compliant business messaging, use the official WhatsApp Business Cloud API.

What whatsmeow is (and why it matters)

whatsmeow is a Go library that speaks WhatsApp's multidevice Web protocol — the same protocol that WhatsApp Web and the desktop app use to link as a companion device to your phone's account. It is written and maintained by Tulir Asokan, the developer behind the mautrix bridge ecosystem, and it is the engine inside mautrix-whatsapp, which in turn powers Beeper's WhatsApp integration. That production lineage matters: a library that quietly underpins a live, paid product tends to get fixed quickly when WhatsApp changes something on their end.

The first thing to get right is the import path. The source repository lives at github.com/tulir/whatsmeow, but the canonical module path you put in go.mod and your imports is go.mau.fi/whatsmeow. That go.mau.fi vanity domain is the correct one. You will see mirror or fork paths on pkg.go.dev — ignore those and use go.mau.fi.

Licensing is MPL-2.0 (Mozilla Public License 2.0), a file-level copyleft license that is permissive enough to link into commercial or closed-source applications. Do not confuse this with the Matrix bridge built on top of it: mautrix-whatsapp is AGPL-3.0, a much stricter license. The two are separate projects with separate obligations.

How the protocol and library are structured

Under the hood, whatsmeow implements the same primitives as a real WhatsApp companion device. It opens a WebSocket connection and performs a Noise-protocol handshake, then exchanges WhatsApp's binary, XMPP-like node format. Message payloads are protobuf, and they are end-to-end encrypted with the Signal protocol's double-ratchet scheme — the library bundles the libsignal-style cryptography so your messages are encrypted the same way the official clients encrypt them.

Because it links as a companion device, whatsmeow needs a phone account to attach to (exactly like scanning a QR code in WhatsApp Web). It is not a standalone account or a number-rental service. Session state — your device keys, identity, and sync data — is held in a store. The usual choice is sqlstore, backed by SQLite for single-instance setups or Postgres when you want durability and horizontal access.

One important modernization to be aware of: the protobuf types were moved out of the now-deprecated go.mau.fi/whatsmeow/binary/proto package into discrete go.mau.fi/whatsmeow/proto/wa* packages — for example waE2E for end-to-end message types, waWeb, and waWa6. If a tutorial imports binary/proto, it predates this restructure and its message-construction code will not compile against current versions. Reach for proto/waE2E instead. Likewise, sqlstore.New and GetFirstDevice now take a context.Context as their first argument; older snippets that omit it will not build.

  • Transport: WebSocket + Noise-protocol handshake
  • Wire format: WhatsApp's binary XMPP-like nodes
  • Payloads: protobuf (proto/waE2E and friends)
  • Encryption: Signal double-ratchet, end-to-end
  • Persistence: store interface, typically sqlstore (SQLite/Postgres)

Pairing: QR code or phone-number code

whatsmeow supports the two pairing flows you already know from WhatsApp Web. The first is the QR code flow: you request a channel that emits QR code strings, render them (in a terminal, image, or web page), and the user scans from their phone under Linked Devices. The channel also emits success and timeout events so you can react when the link completes or expires. Always check the error GetQRChannel returns — it fails if the device is already logged in.

GO
// QR pairing: get a channel of QR events, then Connect.
qrChan, err := client.GetQRChannel(context.Background())
if err != nil {
    // e.g. ErrQRStoreContainsID if the device is already paired.
    log.Fatalf("failed to get QR channel: %v", err)
}
if err := client.Connect(); err != nil {
    log.Fatal(err)
}
for evt := range qrChan {
    switch evt.Event {
    case "code":
        // Render evt.Code as a QR for the user to scan.
        fmt.Println("Scan this code:", evt.Code)
    case "success":
        fmt.Println("Paired and logged in")
    case "timeout":
        fmt.Println("QR expired, request a new one")
    }
}

The second flow is the phone-number pairing code, useful for headless servers where rendering a scannable QR is awkward. You still call Connect() first, then ask whatsmeow for a short code that the user types into their phone — no scanning needed. The returned string is an 8-character code formatted with a hyphen, like ABCD-1234, so display it verbatim rather than stripping the dash.

GO
// Phone-number pairing: returns an 8-char code (e.g. "ABCD-1234") to type on the phone.
if err := client.Connect(); err != nil {
    log.Fatal(err)
}
code, err := client.PairPhone(
    context.Background(),
    "15551234567",                 // phone number, no + or spaces
    true,                          // show push notification
    whatsmeow.PairClientChrome,    // client type
    "Chrome (Linux)",              // display name shown on the phone
)
if err != nil {
    log.Fatal(err)
}
fmt.Println("Enter this code on your phone:", code) // prints e.g. ABCD-1234

A minimal send-and-receive example

Here is the shape of a complete program: open a store, create a client, register an event handler, connect, and send a text message. Note the import of proto/waE2E for building the message, and types/events for the receive side. This example uses the pure-Go modernc.org/sqlite driver (dialect "sqlite") rather than the CGO-based mattn/go-sqlite3 — see the next section for why that choice matters for deployment. Errors are checked deliberately; production code should never discard them with _.

GO
package main

import (
    "context"
    "fmt"
    "log"

    "go.mau.fi/whatsmeow"
    "go.mau.fi/whatsmeow/proto/waE2E"
    "go.mau.fi/whatsmeow/store/sqlstore"
    "go.mau.fi/whatsmeow/types"
    "go.mau.fi/whatsmeow/types/events"
    waLog "go.mau.fi/whatsmeow/util/log"
    "google.golang.org/protobuf/proto"
    _ "modernc.org/sqlite" // pure-Go driver, no CGO; registers "sqlite"
)

func main() {
    ctx := context.Background()
    dbLog := waLog.Stdout("DB", "INFO", true)
    container, err := sqlstore.New(ctx, "sqlite", "file:session.db?_pragma=foreign_keys(1)", dbLog)
    if err != nil {
        log.Fatalf("open store: %v", err)
    }
    device, err := container.GetFirstDevice(ctx)
    if err != nil {
        log.Fatalf("get device: %v", err)
    }

    client := whatsmeow.NewClient(device, waLog.Stdout("Client", "INFO", true))
    client.AddEventHandler(func(evt interface{}) {
        if msg, ok := evt.(*events.Message); ok {
            fmt.Println("Incoming:", msg.Message.GetConversation())
        }
    })

    // Pair via QR or PairPhone here if device.ID == nil (not yet linked).
    if err := client.Connect(); err != nil {
        log.Fatalf("connect: %v", err)
    }
    if !client.IsLoggedIn() {
        log.Fatal("not logged in: pair the device first (QR or PairPhone)")
    }

    jid := types.NewJID("15551234567", types.DefaultUserServer)
    if _, err := client.SendMessage(ctx, jid, &waE2E.Message{
        Conversation: proto.String("Hello from whatsmeow"),
    }); err != nil {
        log.Fatalf("send: %v", err)
    }
}

From there the API surface is small and predictable: Connect(), IsConnected() and IsLoggedIn() for state, SendMessage(ctx, jid, message, ...extra), Download() and Upload() for media, and AddEventHandler for everything that arrives — messages, receipts, group changes, and app-state updates. Note that this API surface reflects whatsmeow as of mid-2026; because it is pre-1.0, signatures can shift between commits, so verify against the version you pin.

What it can and cannot do

whatsmeow covers most of what a companion device does, but not everything. It is worth knowing the boundaries up front so you don't design around a feature that isn't there. Note that the project ships newsletter/channel support that its short README feature list doesn't always spell out — the Client type exposes CreateNewsletter, FollowNewsletter, GetNewsletterMessages, NewsletterSendReaction, and related calls.

CapabilitySupported in whatsmeow
1:1 and group text + media messagingYes
Receiving all messagesYes
Group management and group eventsYes
Invite linksYes
Typing notificationsYes
Delivery and read receiptsYes
App-state sync (contacts, chat settings)Yes
Retry receiptsYes
Newsletter / channel messagesYes
Status messagesExperimental
Broadcast list messagesNo (not supported on WhatsApp Web either)
Voice / video callsNo

Why Go, and whatsmeow vs Baileys

The runtime is the main reason teams pick whatsmeow over the Node.js sibling, Baileys. Go compiles to native machine code with no separate runtime to install, which makes container images tiny and deployment trivial. Memory footprint is typically lower than an equivalent Node process, and goroutines give you cheap native concurrency — handy when one process juggles many simultaneous sessions and a firehose of events.

Baileys covers the same protocol category (multidevice WhatsApp Web) and has a larger JavaScript/TypeScript ecosystem and arguably a gentler on-ramp for web developers. Neither is more 'official' than the other — both are unofficial reverse-engineered clients. The choice is mostly about which runtime your team operates comfortably.

Dimensionwhatsmeow (Go)Baileys (Node.js)
LanguageGoTypeScript / JavaScript
DeploymentNative binary (static if you avoid CGO)Node runtime + node_modules
Concurrency modelGoroutines (native)Event loop / async
Memory footprintLower, typicallyHigher, typically
ProtocolWhatsApp multideviceWhatsApp multidevice
Official statusUnofficialUnofficial
Notable production usermautrix-whatsapp / BeeperMany community bots

For a deeper look at the Node.js side, see our guide on Baileys.

A note on Rhymen/go-whatsapp (deprecated)

If your search turned up github.com/Rhymen/go-whatsapp, stop. That library implemented the old, pre-multidevice WhatsApp Web protocol, which WhatsApp retired during the multidevice migration. It no longer works against current WhatsApp infrastructure, and its own README now explicitly directs users to tulir/whatsmeow. Do not start a new project on it, and if you inherit one, plan a migration — the protocols are different enough that it's effectively a rewrite onto whatsmeow's API.

Terms of Service and ban risk: the honest picture

This is the part too many tutorials skip. whatsmeow is an unofficial, reverse-engineered client. Using it can violate WhatsApp's Terms of Service, and it carries a genuine risk of account bans. This isn't theoretical: tulir/whatsmeow issue #810, opened in May 2025, documents WhatsApp pushing 'your account may be at risk… unauthorized tools' warnings and bans to whatsmeow users — and the same wave hit Baileys users too. Crucially, those warnings and bans landed on low-volume, reply-only, otherwise legitimate usage, not just on bulk spammers.

A pragmatic middle ground for many teams: keep heavy or sensitive operations off the WhatsApp account entirely. If all you need is to verify whether a number is on WhatsApp, fetch a public profile picture and display name, or detect a business account, a hosted profile API does that read-only work without linking — and therefore without risking — one of your own accounts. See the related reading on checking if a number is on WhatsApp and avoiding bans for where that line sits.

Frequently asked questions

Need WhatsApp lookups without the ban risk?

Verify numbers, fetch public profile pictures and display names, and detect business accounts with a hosted, read-only API — no account to link, no protocol to maintain. Free tier to start.

Explore the WhatsApp Profile API
Sponsored

Need cheap numbers to create, verify or warm up WhatsApp accounts for testing? GrizzlySMS rents virtual numbers for WhatsApp verification from under $1.

Get a WhatsApp number

Sources & further reading

Related

More from the blog

Written by
Eduardo Airaudo

Developer and founder of the WhatsApp Profile API. Building WhatsApp tooling and APIs since 2022.

What Our Users Say

Real reviews from our satisfied customers

4.5/5 (170 reviews)