> ## Documentation Index
> Fetch the complete documentation index at: https://zapo.to/llms.txt
> Use this file to discover all available pages before exploring further.

# Conexões mobile

> Conecte o zapo como client mobile (Android) primário do WhatsApp pelo transporte TCP, em vez de dispositivo companheiro, e veja as limitações envolvidas.

Além do modo **companion** padrão (vinculação via QR / código de pareamento, como o WhatsApp Web), o `zapo` pode conectar como um **client mobile primário** — falando o protocolo do app Android por um socket TCP bruto.

<Note>
  O suporte mobile é **estável e funcional.** A única coisa que o `zapo` **não** fornece é uma **API de registro** — solicitar um código por SMS/voz, enviar um OTP ou aprovar um takeover. Registrar um número é complexo e requer um telefone físico, então está intencionalmente fora de escopo. Você conecta com um conjunto de credenciais **já registrado**, e esse caminho é sólido.
</Note>

<h2 id="how-it-differs-from-companion-mode">
  Como difere do modo companion
</h2>

|                     | Companion (padrão)        | Mobile                                                   |
| ------------------- | ------------------------- | -------------------------------------------------------- |
| Transporte          | WebSocket (`wss://…`)     | Socket TCP (`tcp://g.whatsapp.net:443`)                  |
| Auth                | QR / código de pareamento | Credenciais pré-registradas + fingerprint do dispositivo |
| Identidade          | Dispositivo vinculado     | Conta primária                                           |
| Plataforma          | Navegador (`chrome`, …)   | `android`                                                |
| Info do dispositivo | Não obrigatória           | **Obrigatória** (fingerprint de hardware)                |

<h2 id="enabling-mobile-mode">
  Habilitando o modo mobile
</h2>

O modo mobile é acionado pela opção `mobileTransport` (um `WaMobileTransportOptions`). Sua presença — ou um `deviceInfo` persistido nas credenciais carregadas — muda o client do transporte WebSocket para o transporte TCP.

```ts theme={null}
const client = new WaClient(
  {
    store,
    sessionId: 'mobile',
    mobileTransport: {
      deviceInfo: {
        manufacturer: 'OnePlus',
        device: 'OnePlus8Pro',
        osVersion: '12',
        osBuildNumber: 'SKQ1.210216.001',
        appVersion: '2.23.1.1',
        mcc: '55',  // mobile country code (opcional)
        mnc: '11'   // mobile network code (opcional)
      },
      passive: false // envia keep-alives
    }
  },
  logger
)

await client.connect()
```

<h3 id="wamobiletransportoptions">
  `WaMobileTransportOptions`
</h3>

| Campo                    | Tipo                          | Notas                                                  |
| ------------------------ | ----------------------------- | ------------------------------------------------------ |
| `deviceInfo`             | `WaMobileTransportDeviceInfo` | Fingerprint de hardware **obrigatório** (veja abaixo). |
| `tcpUrl`                 | `string`                      | Padrão `tcp://g.whatsapp.net:443`.                     |
| `passive`                | `boolean`                     | `false` envia keep-alives; `true` fica ocioso.         |
| `pushName`               | `string`                      | Nome de exibição.                                      |
| `yearClass` / `memClass` | `number`                      | Classe de performance/memória do dispositivo.          |

<h3 id="wamobiletransportdeviceinfo">
  `WaMobileTransportDeviceInfo`
</h3>

`manufacturer`, `device`, `osVersion`, `osBuildNumber`, `appVersion` são obrigatórios; `mcc`, `mnc`, `localeLanguageIso6391`, `localeCountryIso31661Alpha2`, `phoneId`, `deviceBoard`, `deviceModelType` são opcionais. Um fingerprint **estável** entre execuções importa — persista-o e reutilize os mesmos valores.

<h2 id="credentials">
  Credenciais
</h2>

O modo mobile precisa de um conjunto de credenciais **já registrado**: um `WaAuthCredentials` com `meJid` preenchido, `platform: 'android'` e `deviceInfo` anexado. Você semeia isso na auth store antes de conectar (por exemplo, importado de um device bundle).

<Note>
  Uma vez que as credenciais com `deviceInfo` são persistidas, reconexões posteriores rodam **automaticamente** em modo mobile-primary completo — transporte TCP, formatos de IQ / message id mobile, gating de app-state primary e withholding de placeholder-resend (veja abaixo) tudo deriva do `deviceInfo` carregado. Você não precisa re-passar `mobileTransport` em cada construção.
</Note>

<h3 id="placeholder-resend-withholding">
  Withholding de placeholder-resend
</h3>

Quando um device companion falha em decifrar uma mensagem recebida, ele normalmente pede a um peer pareado o plaintext original via um placeholder-resend request. Um **telefone primário não tem peer device** segurando o plaintext, então uma sessão mobile-primary **pula o placeholder request inteiro** e cai para um retry receipt comum — o caminho padrão de re-encrypt que o sender já suporta. Isso evita o request expirar silenciosamente e a mensagem ser dropada.

<h2 id="registration-events">
  Eventos de registro
</h2>

Enquanto sua sessão mobile está conectada, você é notificado quando **alguém tenta registrar seu número em outro dispositivo** — um sinal relevante para segurança, exposto como estes eventos:

```ts theme={null}
client.on('mobile_registration_code', (event) => {
  // WaRegistrationCodeEvent — alguém solicitou um código para registrar o SEU número em outro lugar
  console.log('registration code issued:', event.code, 'expires:', event.expiryTimestampMs)
})

client.on('mobile_account_takeover_notice', (event) => {
  // WaAccountTakeoverNoticeEvent — outro dispositivo está assumindo o seu número
  console.log('takeover attempt from', event.newDevicePlatform, event.newDeviceName)
})
```

| Evento                           | Payload                                                                                         | Significado                                                                                                                |
| -------------------------------- | ----------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- |
| `mobile_registration_code`       | `{ code, expiryTimestampMs, fromDeviceId }`                                                     | Alguém solicitou um código de registro para registrar o **seu** número em outro telefone; o código emitido é exposto aqui. |
| `mobile_account_takeover_notice` | `{ serverToken, attemptTimestampMs, newDeviceName?, newDevicePlatform?, newDeviceAppVersion? }` | Outro dispositivo está reivindicando (assumindo) o seu número.                                                             |

<Note>
  Esses eventos são **informativos** — o `zapo` os expõe, mas intencionalmente não disponibiliza métodos para enviar um código ou responder a um takeover. O provisionamento de um número é feito em um telefone real; traga as credenciais resultantes para o zapo e conecte.
</Note>

<h2 id="email-binding">
  Vínculo de email
</h2>

<Badge color="orange" icon="mobile">Mobile-only</Badge>

`client.email` ([`WaEmailCoordinator`](/pt-br/reference/client)) vincula e verifica um endereço de email na conta — um fator de recuperação/login. É **mobile-only**: todo método lança erro em uma conexão Web/companion.

```ts theme={null}
// Estado atual do vínculo
const status = await client.email.getStatus()
// { email: string | null, verified: boolean, confirmed: boolean }

// Vincule um endereço, peça um código, envie-o e então confirme
await client.email.setEmail('me@example.com')
await client.email.requestVerificationCode({ /* BuildRequestEmailVerificationCodeInput */ })
const result = await client.email.verifyCode('123456')
// { verified, autoVerifyFailed, email }
await client.email.confirm()
```

<h2 id="hosting-companion-devices">
  Hospedando dispositivos companheiros
</h2>

<Badge color="orange" icon="mobile">Apenas mobile-primary</Badge>

Uma sessão companion vive do outro lado de um QR / código de pareamento, vinculada por um telefone real. Quando o zapo *é* o telefone, os papéis se invertem: esta sessão primary pode **hospedar companions** — vincular uma aba do WhatsApp Web, revogá-la, listar o que está conectado e reconciliar contra a lista de dispositivos do servidor. O coordinator fica em `client.mobile` ([`WaMobileCoordinator`](/pt-br/reference/client)).

<Note>
  `client.mobile` requer uma sessão mobile-primary. Ler o getter em uma conexão Web/companion retorna o coordinator, mas os métodos de link/revoke/publish lançam `client.mobile requires a mobile-primary session (…)` antes de tocar na rede. `reconcileCompanions()` vira um no-op em sessões não-primary.
</Note>

<h3 id="methods">
  Métodos
</h3>

| Método                | Assinatura                                                         | Descrição                                                                                                                                                                                                                                                                                                                |
| --------------------- | ------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `linkCompanion`       | `(qr: string) => Promise<LinkCompanionResult>`                     | Vincula um companion escaneando sua string de QR de pareamento. Retorna `{ deviceJid, keyIndex }`.                                                                                                                                                                                                                       |
| `linkCompanionByCode` | `(pairingCode: string) => Promise<LinkCompanionResult>`            | Vincula pelo código de pareamento de 8 caracteres (fluxo "vincular com número de telefone"). O companion precisa ter solicitado um código para esta conta antes; sem um `companion_hello` pendente, a chamada lança.                                                                                                     |
| `revokeCompanion`     | `(deviceJid: string, reason?: string) => Promise<void>`            | Desvincula um companion hospedado, remove-o do conjunto rastreado e republica a key-index list. `reason` tem default `'user_initiated'`. Lança quando o companion não é rastreado por este primary.                                                                                                                      |
| `revokeAllCompanions` | `(reason?: string, { excludeHostedCompanion? }?) => Promise<void>` | O "log out all companion devices" do telefone — envia `remove-companion-device all="true"`. Com `excludeHostedCompanion: true`, poupa os companions que esta conta hospeda.                                                                                                                                              |
| `listCompanions`      | `() => Promise<readonly CompanionRecord[]>`                        | Os companions rastreados no epoch atual. Cada `CompanionRecord` carrega `{ deviceJid, keyIndex, companionIdentityPublicKey, addedAtSeconds }`.                                                                                                                                                                           |
| `reconcileCompanions` | `() => Promise<readonly string[]>`                                 | Sincroniza o conjunto rastreado com a lista de dispositivos ao vivo do servidor (`usync`), removendo qualquer companion que o servidor não lista mais. Roda automaticamente no connect e em `account_sync`; seguro chamar manualmente. Retorna os device jids removidos. É um no-op quando nenhum companion é rastreado. |
| `publishKeyIndexList` | `() => Promise<void>`                                              | Re-assina e republica a key-index list para o conjunto atual de dispositivos (raro — é chamado por você após link/revoke).                                                                                                                                                                                               |

<h3 id="events">
  Eventos
</h3>

Atividade de companion-host aparece no client:

| Evento                   | Payload                                   | Quando                                                                                                                                                                       |
| ------------------------ | ----------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `companion_host_linked`  | `{ deviceJid: string, keyIndex: number }` | Um companion foi vinculado com sucesso via `linkCompanion` / `linkCompanionByCode`.                                                                                          |
| `companion_host_revoked` | `{ deviceJid: string }`                   | Um companion hospedado foi removido — via `revokeCompanion` / `revokeAllCompanions`, ou removido durante uma reconciliação após o usuário desvinculá-lo em outra superfície. |
| `companion_host_error`   | `Error`                                   | Uma etapa de link ou provisioning em background falhou.                                                                                                                      |

<h3 id="bootstrap-gates-handled-for-you">
  Bootstrap gates cuidados por você
</h3>

Todo companion que um telefone WhatsApp vincula hoje espera três sinais no pair-time. O `zapo` os fornece para que o companion não se remova sozinho após o QR:

* **Client-props de LID migration** — um primary LID-native declara `isChatDbLidMigrated` e `isSyncdPureLidSession` no elemento `<client-props>` para que o companion rode `setIsLidMigrated` no pair-time e não se remova sozinho por conta da sua blocklist endereçada por LID.
* **History-sync `INITIAL_BOOTSTRAP`** — o primary envia a notificação inicial de history-sync como uma peer message; sem ela, o companion se remove sozinho com `HistorySyncTimeout`.
* **`setting_pushName` semeado** — o nome de exibição do próprio primary é semeado na collection `critical_block` do app-state, uma vez por sessão, para o critical bootstrap do companion completar.

Todos os três são automáticos; nenhuma chamada de método necessária.

<h3 id="persistence">
  Persistência
</h3>

O estado do epoch ADV (`rawId`, `currentKeyIndex`, companions rastreados) **precisa** persistir entre reinícios — reusar um índice de chave companion já emitido quebra dispositivos previamente vinculados. Wire um `CompanionHostPersistence` em `WaClientOptions.companionHost.persistence` e o zapo carrega/salva transparentemente.

Uma implementação file-backed vem embarcada com a lib:

```ts theme={null}
import { createFileCompanionHostPersistence, WaClient } from 'zapo-js'

const client = new WaClient({
  store,
  sessionId: 'primary',
  mobileTransport: { deviceInfo: /* ... */ },
  companionHost: {
    persistence: createFileCompanionHostPersistence('./companion-host.json')
  }
}, logger)
```

<Warning>
  Sem hook de persistência o epoch é apenas em memória — bom pra smoke test, **inseguro pra relink em produção**. No restart o primary re-emitiria o key index 1 pra um novo companion enquanto o companion previamente linkado ainda o carrega, quebrando decrypt de sessão no device mais antigo.
</Warning>

<h4 id="custom-backend">
  Backend custom
</h4>

O contrato é dois métodos sobre `CompanionHostEpochState`:

```ts theme={null}
interface CompanionHostPersistence {
  load(): CompanionHostEpochState | null | Promise<CompanionHostEpochState | null>
  save(state: CompanionHostEpochState): void | Promise<void>
}

interface CompanionHostEpochState {
  readonly rawId: number             // id ADV estável da conta
  readonly currentKeyIndex: number   // último key index emitido; próximo companion recebe +1
  readonly companions: readonly CompanionRecord[]
}

interface CompanionRecord {
  readonly deviceJid: string
  readonly keyIndex: number
  readonly companionIdentityPublicKey: Uint8Array
  readonly addedAtSeconds: number
}
```

Aponte pra qualquer store que você já roda. O estado é pequeno (header do epoch + uma linha por companion linkado), então não há necessidade de um domínio de core-store dedicado. Exemplo contra `better-sqlite3`:

```ts theme={null}
import Database from 'better-sqlite3'
import type {
  CompanionHostEpochState,
  CompanionHostPersistence,
  CompanionRecord
} from 'zapo-js'

function createSqliteCompanionHostPersistence(
  db: Database.Database,
  sessionId: string
): CompanionHostPersistence {
  db.exec(`
    CREATE TABLE IF NOT EXISTS companion_host_epoch (
      session_id       TEXT PRIMARY KEY,
      raw_id           INTEGER NOT NULL,
      current_key_index INTEGER NOT NULL
    );
    CREATE TABLE IF NOT EXISTS companion_host_devices (
      session_id                     TEXT NOT NULL,
      device_jid                     TEXT NOT NULL,
      key_index                      INTEGER NOT NULL,
      companion_identity_public_key  BLOB NOT NULL,
      added_at_seconds               INTEGER NOT NULL,
      PRIMARY KEY (session_id, device_jid)
    );
  `)
  const readEpoch = db.prepare<[string]>(
    'SELECT raw_id AS rawId, current_key_index AS currentKeyIndex FROM companion_host_epoch WHERE session_id = ?'
  )
  const readDevices = db.prepare<[string]>(
    'SELECT device_jid AS deviceJid, key_index AS keyIndex, companion_identity_public_key AS pk, added_at_seconds AS addedAtSeconds FROM companion_host_devices WHERE session_id = ?'
  )
  const upsertEpoch = db.prepare(
    'INSERT INTO companion_host_epoch (session_id, raw_id, current_key_index) VALUES (?, ?, ?) ON CONFLICT(session_id) DO UPDATE SET raw_id = excluded.raw_id, current_key_index = excluded.current_key_index'
  )
  const clearDevices = db.prepare('DELETE FROM companion_host_devices WHERE session_id = ?')
  const insertDevice = db.prepare(
    'INSERT INTO companion_host_devices (session_id, device_jid, key_index, companion_identity_public_key, added_at_seconds) VALUES (?, ?, ?, ?, ?)'
  )

  return {
    load(): CompanionHostEpochState | null {
      const header = readEpoch.get(sessionId) as { rawId: number; currentKeyIndex: number } | undefined
      if (!header) return null
      const rows = readDevices.all(sessionId) as Array<{
        deviceJid: string
        keyIndex: number
        pk: Buffer
        addedAtSeconds: number
      }>
      const companions: CompanionRecord[] = rows.map((row) => ({
        deviceJid: row.deviceJid,
        keyIndex: row.keyIndex,
        companionIdentityPublicKey: new Uint8Array(row.pk),
        addedAtSeconds: row.addedAtSeconds
      }))
      return { rawId: header.rawId, currentKeyIndex: header.currentKeyIndex, companions }
    },
    save(state: CompanionHostEpochState): void {
      db.transaction(() => {
        upsertEpoch.run(sessionId, state.rawId, state.currentKeyIndex)
        clearDevices.run(sessionId)
        for (const c of state.companions) {
          insertDevice.run(
            sessionId,
            c.deviceJid,
            c.keyIndex,
            Buffer.from(c.companionIdentityPublicKey),
            c.addedAtSeconds
          )
        }
      })()
    }
  }
}
```

O `save` roda a cada mudança de estado por `linkCompanion` / `revokeCompanion` / `reconcileCompanions`, então envolva a escrita multi-statement numa transaction pra manter atomicidade. `load` é chamado uma vez durante o wire-up do coordinator.

<h3 id="worked-example">
  Exemplo trabalhado
</h3>

```ts theme={null}
import { WaClient } from 'zapo-js'

const client = new WaClient(
  { store, sessionId: 'primary', mobileTransport: { deviceInfo } },
  logger
)

client.on('companion_host_linked', ({ deviceJid, keyIndex }) => {
  console.log('linked companion', deviceJid, 'at key index', keyIndex)
})

client.on('companion_host_revoked', ({ deviceJid }) => {
  console.log('companion revoked:', deviceJid)
})

client.on('companion_host_error', (error) => {
  console.warn('companion-host operation failed', error.message)
})

await client.connect()

// Solicite uma string de QR do WhatsApp Web, depois vincule:
const qr = await readCompanionQrFromSomewhere()
const { deviceJid } = await client.mobile.linkCompanion(qr)

// Depois, liste o que está conectado ou revogue:
const companions = await client.mobile.listCompanions()
await client.mobile.revokeCompanion(deviceJid)
```

<h2 id="standard-features-still-apply">
  Funcionalidades padrão continuam valendo
</h2>

Uma vez conectado no modo mobile, o resto da API permanece inalterado — `client.message`, `client.group`, eventos, stores, etc. funcionam todos da mesma forma. A única diferença é o transporte e o modelo de auth/identidade.
