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:
- Sync inicial pos-pareamento (
messaging-history.set) — agenda inteira de uma vez. Hoje vem so como LID puro (privacidade Meta). - Eventos em tempo real (
contacts.upsert/contacts.update) — contatos novos/editados. - Mensagens recebidas — toda mensagem inbound enriquece o registro do remetente com
phone_numberreal (viasenderPnda key Baileys).
Pra um chip ja pareado antes de habilitar isso, fazer
repairre-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_URLconst ADMIN_KEY = process.env.WPPHUB_ADMIN_KEYconst 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 horasetInterval(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 leadasync function ingestNewLead({ phone, lid, name, photoUrl }) { await db.customer.create({ phone, wpphub_lid: lid, name, source: 'whatsapp_inbound', avatar_url: photoUrl, })}Cuidados
-
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). -
Privacidade Meta — alguns contatos NUNCA expoe telefone (privacidade total). Voce so vai ter
lid+notify. Trate isso como dado opaco. -
Foto expira —
imgUrloriginal do Baileys e URL temporaria. Hub cacheia no MinIO. Se foto mudar (Baileys enviaimgUrl='changed'), proximo lazy fetch baixa nova versao + cria nova media row (antiga preservada — historico forense). -
namevsnotify—name(agenda do dono) e null em 95% dos casos por privacidade. Usenotify(pushName editavel) como display fallback. Em pior caso, mostraphone_numberformatado. -
Volume — 1 chip pode ter milhares de contatos. Indexe
chipId+phone_numberEchipId+lidno seu DB pra lookup rapido.