zapo can connect as a primary mobile client — speaking the Android app’s protocol over a raw TCP socket.
Mobile support is stable and functional. The one thing
zapo does not provide is a registration API — requesting an SMS/voice code, submitting an OTP, or approving a takeover. Registering a number is complex and requires a physical phone, so it’s intentionally out of scope. You connect with an already-registered credential set, and that path is solid.How it differs from companion mode
Enabling mobile mode
Mobile mode is triggered by themobileTransport option (a WaMobileTransportOptions). Its presence — or persisted deviceInfo in the loaded credentials — switches the client from the WebSocket transport to the TCP transport.
WaMobileTransportOptions
WaMobileTransportDeviceInfo
manufacturer, device, osVersion, osBuildNumber, appVersion are required; mcc, mnc, localeLanguageIso6391, localeCountryIso31661Alpha2, phoneId, deviceBoard, deviceModelType are optional. A stable fingerprint across runs matters — persist it and reuse the same values.
Credentials
Mobile mode needs an already-registered credential set: aWaAuthCredentials with meJid populated, platform: 'android', and deviceInfo attached. You seed these into the auth store before connecting (e.g. imported from a device bundle).
Once credentials with
deviceInfo are persisted, later reconnects automatically run in full mobile-primary mode — TCP transport, mobile-style IQ / message id formats, app-state primary gating, and placeholder-resend withholding (see below) all derive from the loaded deviceInfo. You don’t need to re-pass mobileTransport on every construction.Placeholder-resend withholding
When a companion device fails to decrypt an incoming message, it normally asks a paired peer for the original plaintext via a placeholder-resend request. A primary phone has no peer device holding the plaintext, so a mobile-primary session skips the placeholder request entirely and falls back to a plain retry receipt — the standard re-encrypt path the sender already supports. This avoids the request silently timing out and the message being dropped.Registration events
While your mobile session is connected, you’re notified when someone tries to register your number on another device — a security-relevant signal, surfaced as these events:These events are informational —
zapo surfaces them but intentionally does not expose methods to submit a code or respond to a takeover. Provisioning a number is done on a real phone; bring the resulting credentials to zapo and connect.Email binding
Mobile-onlyclient.email (WaEmailCoordinator) binds and verifies an email address on the account — a recovery/login factor. It is mobile-only: every method throws on a Web/companion connection.
Hosting companion devices
Mobile-primary only A companion session lives on the other side of a QR / pairing code, linked by a real phone. When zapo is the phone, the roles invert: this primary session can host companions — link a WhatsApp Web tab, revoke it, list what’s connected, and reconcile against the server’s device list. The coordinator sits atclient.mobile (WaMobileCoordinator).
client.mobile requires a mobile-primary session. Reading the getter on a Web/companion connection returns the coordinator, but the link/revoke/publish methods throw with client.mobile requires a mobile-primary session (…) before touching the network. reconcileCompanions() becomes a no-op on non-primary sessions.Methods
Events
Companion-host activity surfaces on the client:Bootstrap gates handled for you
Every companion a WhatsApp phone links today expects three signals during pair-time.zapo supplies them so the companion doesn’t self-remove after the QR scan:
- LID migration client-props — a LID-native primary declares
isChatDbLidMigratedandisSyncdPureLidSessionon the<client-props>element so the companion runssetIsLidMigratedat pair time and doesn’t self-remove on its LID-addressed blocklist. INITIAL_BOOTSTRAPhistory-sync — the primary pushes the initial history-sync notification as a peer message; without it the companion self-removes withHistorySyncTimeout.- Seeded
setting_pushName— the primary’s own display name is seeded into thecritical_blockapp-state collection, once per session, so the companion’s critical bootstrap can complete.
Persistence
The ADV epoch state (rawId, currentKeyIndex, tracked companions) must persist across restarts — reusing an already-issued companion key index breaks previously linked devices. Wire a CompanionHostPersistence into WaClientOptions.companionHost.persistence and zapo loads/saves it transparently.
A file-backed implementation ships with the library:
Custom backend
The contract is two methods overCompanionHostEpochState:
better-sqlite3:
save runs on every linkCompanion / revokeCompanion / reconcileCompanions state change, so wrap the multi-statement write in a transaction to keep it atomic. load is called once during coordinator wire-up.
Worked example
Standard features still apply
Once connected in mobile mode, the rest of the API is unchanged —client.message, client.group, events, stores, etc. all work the same way. The only difference is the transport and the auth/identity model.