Skip to content

Backup de contatos do chip

WPP Hub persiste a agenda do celular pareado em contacts. Voce pode espelhar no seu sistema pra ter snapshot independente — vital se o chip for banido pelo WhatsApp.

Cenario

Voce mantem um CRM com cadastro de leads/clientes. Quando alguem manda mensagem pro seu chip, voce quer:

  • Ver foto, nome, business profile do contato no inbox
  • Cruzar com cadastro existente (telefone)
  • Backup periodico caso WhatsApp invalide o chip

Origens dos dados (recap)

WPP Hub popula contacts por 3 caminhos:

  1. Sync inicial pos-pareamento (messaging-history.set) — agenda inteira de uma vez. Hoje vem so como LID puro (privacidade Meta).
  2. Eventos em tempo real (contacts.upsert/contacts.update) — contatos novos/editados.
  3. Mensagens recebidas — toda mensagem inbound enriquece o registro do remetente com phone_number real (via senderPn da key Baileys).

Pra um chip ja pareado antes de habilitar isso, fazer repair re-dispara o sync inicial.

Sincronizando com seu DB

Estrategia 1 — pull periodico

Cron 1x/hora que faz dump da lista paginada e atualiza seu DB local.

import 'dotenv/config'
const HUB = process.env.WPPHUB_BASE_URL
const ADMIN_KEY = process.env.WPPHUB_ADMIN_KEY
const CHIP_ID = process.env.CHIP_ID
async function syncContacts() {
let cursor = null
let total = 0
do {
const params = new URLSearchParams({ limit: '200' })
if (cursor) params.set('cursor', cursor)
const res = await fetch(`${HUB}/v1/chips/${CHIP_ID}/contacts?${params}`, {
headers: { Authorization: `Bearer ${ADMIN_KEY}` },
})
const { data, next_cursor, total: t } = await res.json()
total = t
for (const c of data) {
await db.contact.upsert({
where: { chipId_jid: { chipId: c.chip_id, jid: c.jid } },
create: {
chipId: c.chip_id,
jid: c.jid,
lid: c.lid,
phoneNumber: c.phone_number,
name: c.name,
notify: c.notify,
verifiedName: c.verified_name,
statusText: c.status_text,
hasPhoto: c.has_photo,
lastSyncedAt: new Date(c.last_synced_at),
updatedAt: new Date(c.updated_at),
},
update: {
lid: c.lid,
phoneNumber: c.phone_number,
name: c.name,
notify: c.notify,
verifiedName: c.verified_name,
statusText: c.status_text,
hasPhoto: c.has_photo,
lastSyncedAt: new Date(c.last_synced_at),
updatedAt: new Date(c.updated_at),
},
})
}
cursor = next_cursor
} while (cursor)
console.log(`synced ${total} contacts`)
}
// Roda a cada hora
setInterval(syncContacts, 60 * 60 * 1000)
syncContacts()

Estrategia 2 — push via webhook (futuro)

Backlog: webhook contact.updated pra app reagir em tempo real. Hoje nao existe — use estrategia 1.

Cacheando fotos no seu lado

Hub serve foto via GET /v1/chips/:chipId/contacts/:jid/photo (lazy fetch — primeiro hit baixa via Baileys + persiste no MinIO; subsequentes vem direto). Se voce quer independencia total (snapshot offline mesmo se hub cair):

async function backupPhoto(chipId, jid) {
const url = `${HUB}/v1/chips/${chipId}/contacts/${encodeURIComponent(jid)}/photo`
const res = await fetch(url, {
headers: { Authorization: `Bearer ${ADMIN_KEY}` },
})
if (res.status === 404) return null // sem foto OU bloqueado
if (res.status === 503) return null // chip offline, tenta depois
if (!res.ok) throw new Error(`fetch photo failed: ${res.status}`)
const buffer = Buffer.from(await res.arrayBuffer())
const mimetype = res.headers.get('content-type') || 'image/jpeg'
// Salva no seu storage (S3, disk, etc)
await s3.putObject({
Bucket: 'meu-backup',
Key: `wpphub/${chipId}/${jid}.jpg`,
Body: buffer,
ContentType: mimetype,
})
}

Combine com sync de metadados — apos syncContacts(), itera pelos que tem hasPhoto=true e baixa.

Lookup pelo seu CRM

Quando uma mensagem chega, voce quer cruzar com cliente cadastrado:

async function findCRMCustomer(senderPhone, senderLid) {
// 1. Tenta por telefone (caminho mais confiavel)
if (senderPhone) {
const customer = await db.customer.findFirst({ where: { phone: senderPhone } })
if (customer) return customer
}
// 2. Fallback por LID (caso contato com privacidade)
if (senderLid) {
const customer = await db.customer.findFirst({ where: { wpphub_lid: senderLid } })
if (customer) return customer
}
return null
}
// Se nao acha, registra novo lead
async function ingestNewLead({ phone, lid, name, photoUrl }) {
await db.customer.create({
phone,
wpphub_lid: lid,
name,
source: 'whatsapp_inbound',
avatar_url: photoUrl,
})
}

Cuidados

  1. LID-only sync inicial — chips ja pareados antes do listener serem ativados podem ter ate centenas de contatos sem phone_number. Aceite: vai populando aos poucos via mensagens recebidas (3-4 dias e maioria coberta).

  2. Privacidade Meta — alguns contatos NUNCA expoe telefone (privacidade total). Voce so vai ter lid + notify. Trate isso como dado opaco.

  3. Foto expiraimgUrl original do Baileys e URL temporaria. Hub cacheia no MinIO. Se foto mudar (Baileys envia imgUrl='changed'), proximo lazy fetch baixa nova versao + cria nova media row (antiga preservada — historico forense).

  4. name vs notifyname (agenda do dono) e null em 95% dos casos por privacidade. Use notify (pushName editavel) como display fallback. Em pior caso, mostra phone_number formatado.

  5. Volume — 1 chip pode ter milhares de contatos. Indexe chipId+phone_number E chipId+lid no seu DB pra lookup rapido.