Gestão de contatos
Toda mensagem que entra ou sai vira contato na sua base — um CRM leve embutido no gateway. O bZapper captura o contato automaticamente da conversa (nome do WhatsApp, avatar, atividade) e você enriquece com os campos de CRM (e-mail, documento, endereço), organiza com tags e grupos de contato, filtra por qualquer critério e acompanha a timeline de cada pessoa.
Aqui tratamos da base de contatos (pessoas) e dos grupos de contato (segmentos
de CRM), que vivem em /contacts e /contact-groups. Não confunda com os grupos de
WhatsApp (salas de conversa), que ficam em /groups. São coisas diferentes.
Correlação automática
Você não precisa cadastrar nada para começar: a cada mensagem trocada, o bZapper correlaciona o contato ao projeto e ao número que falou com ele. O contato ganha:
instance_id— o último número que conversou com ele (útil para afinidade/sticky).last_message_atemessage_count— atividade viva.source— como entrou na base:inbound,outbound,api,importouwidget.nameeavatar_url— nome de exibição (push name) e foto, best effort.
O telefone é sempre normalizado (+DDIdigits, sem espaços) e o e-mail vira minúsculo
no banco — o mesmo contato é reconhecido sem duplicatas.
Carimbar tags e grupos no envio
Os campos groups[] e tags[] de qualquer envio carimbam o contato
destinatário quando a mensagem resolve — sem uma chamada extra ao CRM. Chaves
desconhecidas são criadas no dicionário na hora.
curl -X POST https://api.bzapper.com.br/messages/text \
-H "Authorization: Bearer $BZ_KEY" -H "X-Project-Id: $PROJECT" \
-H "Content-Type: application/json" \
-d '{
"to": "+5511999998888",
"body": "Bem-vindo! 🎉",
"tags": ["lead-quente"],
"groups": ["onboarding"]
}'
groups[] no envio NÃO manda para grupo de WhatsAppgroups[] correlaciona o contato a grupos de contato (CRM). Para enviar a uma sala
do WhatsApp, use o JID do grupo no campo to.
Perfil do contato
Cada contato tem campos de CRM em colunas separadas — nada de "notas soltas":
| Campo | O que é |
|---|---|
phone | telefone +DDIdigits (obrigatório na criação) |
name | nome (push name do WhatsApp ou o que você informar) |
email | e-mail (armazenado em minúsculo) |
document / document_type | documento (CPF/CNPJ/passaporte…) e seu tipo |
address | endereço em campos separados: street, number, complement, district, city, state, zip, country |
tags / groups | chaves de tags e grupos de contato correlacionados |
status | estado do ciclo de vida (veja abaixo) |
instance_id | último número que conversou com ele |
message_count, last_message_at, created_at, updated_at | atividade |
Estados (status)
| Estado | Significado |
|---|---|
active | ativo, recebe normalmente |
pending_validation | ainda não validado |
opted_out | pediu descadastro (LGPD) — bloqueado para envio |
blocked | bloqueado manualmente por um admin |
unreachable | problema de recebimento (bounce/inferido) |
Criar e atualizar
# Criar (phone obrigatório; o resto é opcional)
curl -X POST https://api.bzapper.com.br/contacts \
-H "Authorization: Bearer $BZ_KEY" -H "X-Project-Id: $PROJECT" \
-H "Content-Type: application/json" \
-d '{
"phone": "+5511999998888",
"name": "Vinicius Berni",
"email": "[email protected]",
"document": "12345678900",
"document_type": "cpf",
"address": { "city": "Porto Alegre", "state": "RS", "country": "BR" }
}'
Resposta 201:
{
"id": "3f2a…",
"phone": "+5511999998888",
"name": "Vinicius Berni",
"email": "[email protected]",
"document": "12345678900",
"document_type": "cpf",
"address": { "city": "Porto Alegre", "state": "RS", "country": "BR" },
"status": "active",
"source": "api",
"message_count": 0,
"tags": [],
"groups": [],
"created_at": "2026-07-01T12:00:00Z",
"updated_at": "2026-07-01T12:00:00Z"
}
Criar com um telefone que já existe devolve 409. A atualização é parcial
(PATCH /contacts/{id}) — mande só os campos que mudaram.
Listar com filtros avançados
GET /contacts aceita um leque de filtros combináveis e paginação por offset:
| Filtro | O que faz |
|---|---|
search | nome, telefone, e-mail ou documento |
tags + tags_match | chaves de tag; casa any (padrão) ou all |
groups | chaves de grupo de contato |
status | active · pending_validation · opted_out · blocked · unreachable |
city · state · country · zip · document | filtros do perfil |
has_email | só quem tem (true) ou não tem (false) e-mail |
instance_id | último número que falou com o contato |
last_activity_after / _before | janela de last_message_at (RFC3339) |
created_after / _before | janela de criação (RFC3339) |
sort | last_activity (padrão) · name · created |
limit / offset | paginação (limit padrão 200, máx. 500) |
# Leads quentes em POA, com e-mail, ativos, ordenados por atividade
curl -G https://api.bzapper.com.br/contacts \
-H "Authorization: Bearer $BZ_KEY" -H "X-Project-Id: $PROJECT" \
--data-urlencode "tags=lead-quente" \
--data-urlencode "city=Porto Alegre" \
--data-urlencode "has_email=true" \
--data-urlencode "status=active"
Resposta:
{ "data": [ { "id": "…", "phone": "+55…", "name": "…", "tags": ["lead-quente"] } ],
"total": 1, "limit": 200, "offset": 0 }
Timeline (histórico)
GET /contacts/{id}/history devolve a linha do tempo unificada — mensagens
(entrada/saída) e eventos (opt-out, notas, mudanças de tag/grupo, transições de
status) — mais recente primeiro.
{
"data": [
{ "kind": "message", "type": "text", "direction": "inbound",
"status": "received", "actor": "contact", "payload": { "body": "oi" },
"created_at": "2026-07-01T12:03:00Z" },
{ "kind": "event", "type": "tag_added", "actor": "api",
"payload": { "tag": "lead-quente" }, "created_at": "2026-07-01T12:00:00Z" }
]
}
Notas internas
POST /contacts/{id}/notes adiciona uma nota interna (auditoria/CRM) — nunca
enviada ao contato; aparece na timeline.
curl -X POST https://api.bzapper.com.br/contacts/$ID/notes \
-H "Authorization: Bearer $BZ_KEY" -H "X-Project-Id: $PROJECT" \
-H "Content-Type: application/json" \
-d '{ "body": "Cliente pediu retorno na sexta." }'
Tags e grupos de contato
Tags e grupos de contato são dicionários próprios do projeto — cada entrada tem
key (slug estável usado na correlação), name, color e a contagem de contatos.
# Criar uma tag
curl -X POST https://api.bzapper.com.br/tags \
-H "Authorization: Bearer $BZ_KEY" -H "X-Project-Id: $PROJECT" \
-H "Content-Type: application/json" \
-d '{ "key": "lead-quente", "name": "Lead quente", "color": "#22c55e" }'
# Criar um grupo de contato (segmento de CRM — NÃO grupo de WhatsApp)
curl -X POST https://api.bzapper.com.br/contact-groups \
-H "Authorization: Bearer $BZ_KEY" -H "X-Project-Id: $PROJECT" \
-H "Content-Type: application/json" \
-d '{ "key": "onboarding", "name": "Onboarding" }'
Listar (GET /tags, GET /contact-groups) devolve as entradas com count. Apagar
(DELETE /tags/{id}, DELETE /contact-groups/{id}) remove do dicionário e desvincula
de todos os contatos.
Correlacionar em lote num contato
# Adiciona/remove tags num contato de uma vez (chaves novas são criadas)
curl -X POST https://api.bzapper.com.br/contacts/$ID/tags \
-H "Authorization: Bearer $BZ_KEY" -H "X-Project-Id: $PROJECT" \
-H "Content-Type: application/json" \
-d '{ "add": ["lead-quente"], "remove": ["frio"] }'
O mesmo formato ({ "add": [...], "remove": [...] }) vale para
POST /contacts/{id}/groups.
Opt-out, supressão e problemas de recebimento
O bloco de privacidade da base tem três ações no contato e uma lista de supressão do projeto:
| Rota | O que faz |
|---|---|
POST /contacts/{id}/optout | marca opted_out (grava opted_out_at) e adiciona à supressão — LGPD |
POST /contacts/{id}/suppress | bloqueio manual: status blocked + entrada de supressão |
POST /contacts/{id}/optin | remove a supressão e reativa (active) |
/contacts/{id}/suppress ≠ /contacts/{jid}/blocksuppress bloqueia o contato no bZapper (não recebe mais envios). Já
/contacts/{jid}/block bloqueia o número no WhatsApp (device-level). São camadas
distintas.
Lista de supressão do projeto
# Listar números suprimidos
curl https://api.bzapper.com.br/suppressions \
-H "Authorization: Bearer $BZ_KEY" -H "X-Project-Id: $PROJECT"
# Adicionar manualmente
curl -X POST https://api.bzapper.com.br/suppressions \
-H "Authorization: Bearer $BZ_KEY" -H "X-Project-Id: $PROJECT" \
-H "Content-Type: application/json" \
-d '{ "phone": "+5511999998888", "reason": "manual" }'
# Remover (un-suppress) pelo telefone
curl -X DELETE "https://api.bzapper.com.br/suppressions?phone=%2B5511999998888" \
-H "Authorization: Bearer $BZ_KEY" -H "X-Project-Id: $PROJECT"
Cada entrada guarda phone, reason (ex.: optout, manual, bounce) e source
(api, admin, contact, system). Para entender a supressão de dois níveis e o
opt-out por palavra-chave, veja Privacidade & LGPD.
Endpoints
| Método | Rota | O que faz |
|---|---|---|
| GET | /contacts | Lista com filtros avançados + paginação |
| POST | /contacts | Cria um contato (phone obrigatório) |
| GET | /contacts/{id} | Detalha um contato |
| PATCH | /contacts/{id} | Atualização parcial dos campos de CRM |
| DELETE | /contacts/{id} | Remove o contato |
| GET | /contacts/{id}/history | Timeline (mensagens + eventos) |
| POST | /contacts/{id}/notes | Adiciona nota interna |
| POST | /contacts/{id}/tags | Correlaciona tags em lote (add/remove) |
| POST | /contacts/{id}/groups | Correlaciona grupos de contato em lote |
| POST | /contacts/{id}/optout · /suppress · /optin | Opt-out / bloqueio / reativação |
| GET · POST | /tags · /contact-groups | Dicionários (com contagem) |
| DELETE | /tags/{id} · /contact-groups/{id} | Remove do dicionário e desvincula |
| GET · POST · DELETE | /suppressions | Lista de supressão do projeto |
Na maioria dos casos você nem chama o CRM diretamente: mande tags[]/groups[] no
envio e deixe o bZapper carimbar os contatos por você.