11. Contatos#
GET/v1/instances/{instanceId}/contacts#
Lista contatos da instância.
Auth: Todos autenticados
Query Parameters:
| Parametro | Tipo | Padrão | Descricao |
|---|---|---|---|
page |
int | 1 | Página |
limit |
int | 20 | Itens por página (max 100) |
Resposta 200:
{
"data": [
{
"jid": "554137984905@s.whatsapp.net",
"push_name": "Joao",
"business_name": "",
"full_name": "Joao Silva",
"first_name": "Joao",
"profile_pic_url": "https://media.catcher.one/.../avatars/554137984905.jpg"
}
],
"total": 15,
"page": 1,
"limit": 20
}
Nota sobre
profile_pic_url: Presente somente quando o avatar do contato já foi cacheado no storage da Catcher. Contatos sem foto ou com foto ainda não cacheada não incluem este campo. O cache e preenchido automaticamente quando o contato muda a foto (events.Picture) ou via lazy load no primeiro acesso ao endpoint/avatar.
GET/v1/instances/{instanceId}/contacts/{contactId}#
Retorna informações de um contato. A resposta vem do banco do Catcher (registro persistente) e opcionalmente e enriquecida com dados ao vivo do WhatsApp (status, avatar atualizado). O endpoint funciona mesmo se a instância estiver desconectada — retorna o último estado conhecido.
Auth: Todos autenticados
Parametros de URL:
| Parametro | Descricao |
|---|---|
instanceId |
ID da instância |
contactId |
Telefone (E.164, com ou sem +), phone JID (554199...@s.whatsapp.net), ou LID (2162...@lid). BR com 9 extra e normalizado automaticamente |
Query params:
| Parametro | Tipo | Padrão | Descricao |
|---|---|---|---|
live |
bool | true |
false desabilita o enrichment com chamada ao WhatsApp (retorna so o estado do banco). Use quando quiser resposta rápida e offline-safe |
Resposta 200:
{
"id": 42,
"jid": "554137984905@s.whatsapp.net",
"phone": "554137984905",
"push_name": "Joao Silva",
"business_name": "",
"full_name": "Joao Silva",
"first_name": "Joao",
"status": "Hey there!",
"about": "Busy",
"picture_id": "122207890",
"picture_url": "https://media.catcher.one/.../avatars/554137984905.jpg",
"first_seen_at": "2026-03-01T12:00:00Z",
"last_seen_at": "2026-04-17T15:30:00Z",
"created_at": "2026-03-01T12:00:00Z",
"updated_at": "2026-04-17T15:30:00Z",
"identities": [
{
"jid": "554137984905@s.whatsapp.net",
"type": "phone",
"first_seen_at": "2026-03-01T12:00:00Z",
"last_seen_at": "2026-04-17T15:30:00Z"
},
{
"jid": "216251804708983@lid",
"type": "lid",
"first_seen_at": "2026-03-15T08:00:00Z",
"last_seen_at": "2026-04-17T15:30:00Z"
}
]
}
Campos da resposta:
| Campo | Tipo | Descricao |
|---|---|---|
id |
int | ID interno do registro (não persistente entre ambientes) |
jid |
string | Phone JID canonico (<phone>@s.whatsapp.net) |
phone |
string | Número sem sufixo (E.164 sem +) |
push_name / business_name / full_name / first_name |
string | Nomes do contato. push_name/business_name/about vem do banco; full_name e first_name são preenchidos apenas com live=true e instância conectada |
status |
string | Status/about ao vivo (apenas com live=true e instância conectada) |
about |
string | About salvo no banco (persistido de eventos UserAbout) |
picture_url |
string | URL do avatar — sempre controlada pela Catcher (S3/R2 público ou endpoint autenticado /avatar). Nunca e URL direta do CDN do WhatsApp |
first_seen_at / last_seen_at |
datetime | Primeira e última interacao conhecida |
identities[] |
array | Todas as variantes conhecidas de JID/LID para este contato. type=phone ou type=lid. Util para consumidores que recebem webhooks com diferentes JIDs e precisam de-duplicar (lesson #5 — WhatsApp não resolve LID→phone ao vivo, so via registry) |
Nota: Quando
live=truee a instância está conectada, o endpoint enriquece a resposta com dados atualizados do WhatsApp (status, nomes, avatar). Quandolive=falseou a instância está desconectada, retorna so o que o Catcher persistiu — resposta sempre disponível.
Nota (politica de proxy — lesson #29):
picture_urlretorna somente URL controlada pela Catcher. Se o avatar ainda não foi cacheado pelo worker (baixado através do proxy ISP da instância e armazenado no S3/R2), o campo não vem na resposta. A API nunca expoe URL temporaria do CDN do WhatsApp pro frontend — fazer isso delegaria o bypass do proxy ao navegador do cliente.
Erros:
| Status | Descricao |
|---|---|
| 200 | Contato encontrado OU identities[] vazio (ainda sem interacao com esse número). Sempre 200 pra permitir o consumidor guardar o JID mesmo antes da primeira mensagem |
GET/v1/instances/{instanceId}/contacts/engagement#
Lista de contatos rica com dados de engajamento — a listagem renderizada na tela /contacts da Console. Combina, numa única query, contacts (nome de exibição + avatar), contact_mutuality_caches (flag mutuo), contact_temperatures (contadores de engajamento) e contagens agregadas de mensagens.
Auth: Todos autenticados
Query Parameters:
| Parametro | Tipo | Padrão | Descricao |
|---|---|---|---|
search |
string | — | Substring (case-insensitive) contra nome OU telefone |
tier |
string | — | Filtra por tier: cold, warm, engaged, hot, very_hot |
mutual |
string | — | yes ou no (omita para ambos) |
page |
int | 1 | Página (1-based) |
limit |
int | 50 | Tamanho da página (max 200) |
Resposta 200:
{
"items": [
{
"phone": "554199999999",
"remote_jid": "554199999999@s.whatsapp.net",
"name": "Joao Silva",
"push_name": "Joao",
"business_name": "",
"is_group": false,
"has_avatar": true,
"profile_pic_url": "https://media.catcher.one/.../avatars/554199999999.jpg",
"is_mutual": true,
"inbound_total": 12,
"outbound_total": 5,
"outbound_consecutive": 1,
"score": 65,
"tier": "hot",
"max_consecutive": 5,
"last_inbound_at": "2026-05-07T13:42:11Z",
"last_outbound_at": "2026-05-07T14:01:00Z"
}
],
"total": 1234,
"page": 1,
"limit": 50
}
| Campo | Tipo | Descricao |
|---|---|---|
phone |
string | Número normalizado (sem sufixo) |
remote_jid |
string | JID da conversa (<phone>@s.whatsapp.net ou <id>@g.us) |
name |
string | Nome de exibição (push_name > business_name > phone) |
push_name / business_name |
string | Nomes do contato |
is_group |
bool | true se o remote_jid e um grupo |
has_avatar |
bool | true se ha avatar cacheado |
profile_pic_url |
string | URL do avatar (controlada pela Catcher) ou vazia |
is_mutual |
bool | null | true/false quando ha cache de mutualidade; null se ainda não verificado |
inbound_total / outbound_total |
int | Total de mensagens recebidas / enviadas |
outbound_consecutive |
int | Outbounds desde o último inbound |
score |
int | Score de temperatura 0-100 |
tier |
string | cold | warm | engaged | hot | very_hot |
max_consecutive |
int | Limite de outbound consecutivo derivado do tier |
last_inbound_at / last_outbound_at |
RFC3339 | null | Timestamps do último inbound/outbound |
Ordenacao primaria:
last_inbound_at DESC. Os filtrostier,mutualesearchsão aplicados pos-computo do score.
GET/v1/instances/{instanceId}/contacts/{contactId}/avatar#
Retorna a imagem de avatar do contato diretamente (proxy via S3/R2). Na primeira requisição para um contato sem cache, busca a foto do WhatsApp, armazena no storage, e retorna a imagem.
Auth: Todos autenticados (JWT ou API Key)
Headers de resposta:
Content-Type: image/jpeg(ou outro tipo conforme a imagem)Cache-Control: public, max-age=86400
Resposta 200: Imagem binaria (JPEG/PNG).
Resposta 404: Contato não encontrado ou sem foto de perfil.
Resposta 503: Storage S3/R2 não configurado.
Comportamento de cache:
- Primeiro acesso: busca a foto do WhatsApp, faz upload no S3/R2, e retorna a imagem (lazy cache).
- Acessos seguintes: serve diretamente do S3/R2 (rápido, sem depender do WhatsApp).
- Mudanca de foto: quando o WhatsApp notifica via
events.Picture, o cache e atualizado automaticamente em background. - Remoção de foto: quando o contato remove a foto, o cache e limpo e o endpoint passa a retornar 404.
Dica para clientes: Use
profile_pic_urlretornado nos endpointsGET /chats,GET /contacts, ouGET /contacts/{id}comosrcde tags<img>. Estas URLs nunca expiram (diferente das URLs do CDN do WhatsApp). SeS3_PUBLIC_URLestiver configurado no servidor, a URL e pública e pode ser usada diretamente; caso contrario, a URL aponta para o endpoint proxy/avatarque requer autenticação.
GET/v1/instances/{instanceId}/contacts/{contactId}/mutuality#
Retorna se phone (URL param contactId) tem essa instância salva na agenda do WhatsApp dele — i.e. são contatos mutuos. Resposta cacheada por 6h via contact_mutuality_cache (tenant). Cache miss dispara um client.IsOnWhatsApp (uSync) através do worker.
Auth: Todos autenticados (JWT ou API Key)
Resposta 200:
{
"is_mutual": true,
"checked_at": "2026-05-06T15:00:00Z",
"phone": "5511999999999"
}
| Campo | Tipo | Descricao |
|---|---|---|
is_mutual |
bool | true quando o servidor da Meta confirma que o número tem essa instância na agenda |
checked_at |
RFC3339 | Quando o resultado foi obtido (cache hit OU uSync). Use para mostrar "verificado ha N min" |
phone |
string | Número normalizado (sem +, sem sufixo @) |
Resposta 503: Instância não conectada ou worker indisponível.
Notas:
- O
contactIdaceita formato puro (5511999999999), com+(+5511999999999), ou JID completo (5511999999999@s.whatsapp.net). Tudo e normalizado para o número limpo. - Cache hit dentro de 6h retorna sem roundtrip.
- Cache stale + uSync com falha retorna o último valor conhecido (graceful fallback).
- Apenas o
info.IsInflag do whatsmeow (a contraparte dois_in_address_bookda Meta).
Temperatura de Contato (Anti-Spam)#
Score de engajamento 0-100 por contato, usado internamente para limitar mensagens outbound consecutivas sem reply inbound (regra de temperatura do anti-spam guard). O score deriva de histórico inbound + recencia + penalty por outbound consecutivo + boost de mutualidade.
Tiers (curva fixa):
cold(0-19) —max_consecutive=2warm(20-39) —max_consecutive=3engaged(40-59) —max_consecutive=4hot(60-79) —max_consecutive=5very_hot(80-100) —max_consecutive=7
max_consecutive e o limite de envios outbound seguidos sem reply do contato; quando atingido, o próximo envio retorna 409 BLOCKED_TEMPERATURE_LIMIT. Bypass via header X-Force-Send: true (audit-logged).
GET/v1/instances/{instanceId}/contacts/{phone}/temperature#
Retorna o score de temperatura corrente para um contato. Bootstraps a linha do contact_temperatures a partir do histórico em messages na primeira chamada (lazy).
Auth: Todos autenticados (JWT ou API Key)
Resposta 200:
{
"phone": "554199999999",
"remote_jid": "554199999999@s.whatsapp.net",
"score": 65,
"tier": "hot",
"max_consecutive": 5,
"inbound_count_total": 8,
"outbound_consecutive": 1,
"last_inbound_at": "2026-05-07T13:42:11Z",
"last_outbound_at": "2026-05-07T14:01:00Z",
"bootstrap_source": "history",
"bootstrapped_at": "2026-04-30T08:15:22Z"
}
| Campo | Tipo | Descricao |
|---|---|---|
phone |
string | Número normalizado |
remote_jid |
string | JID canonico <phone>@s.whatsapp.net |
score |
int | 0-100, computado em runtime a partir dos contadores persistidos |
tier |
string | cold | warm | engaged | hot | very_hot |
max_consecutive |
int | Limite de outbound consecutivo derivado do tier |
inbound_count_total |
int | Total de inbounds na vida do contato |
outbound_consecutive |
int | Outbounds desde o último inbound |
last_inbound_at |
RFC3339 | null | Último inbound recebido |
last_outbound_at |
RFC3339 | null | Último outbound enviado |
bootstrap_source |
string | history | mutuality | cold | manual — evidência usada no bootstrap |
bootstrapped_at |
RFC3339 | null | Quando a linha foi seeded |
Notas:
phoneaceita5511999999999,+5511999999999, ou JID completo. Normalizado.- A mutualidade NÃO e consultada nesta leitura (evita RPC). O score-time mutuality boost so atua via send-path Check.
- Bootstrap pode levar centenas de ms na primeira chamada para um contato com histórico denso (4 queries indexadas em
messages); leituras subsequentes são baratas.
GET/v1/instances/{instanceId}/contacts/temperature#
Lista contatos paginada, filtravel por faixa de score.
Auth: Todos autenticados
Query params:
| Param | Default | Descricao |
|---|---|---|
min_score |
0 | Score mínimo (0-100) |
max_score |
100 | Score máximo (0-100) |
page |
1 | Página (1-based) |
limit |
50 | Tamanho da página (max 200) |
Resposta 200:
{
"items": [
{
"phone": "554199999999",
"remote_jid": "554199999999@s.whatsapp.net",
"score": 65,
"tier": "hot",
"max_consecutive": 5,
"inbound_count_total": 8,
"outbound_consecutive": 1,
"last_inbound_at": "2026-05-07T13:42:11Z",
"last_outbound_at": "2026-05-07T14:01:00Z",
"bootstrap_source": "history",
"bootstrapped_at": "2026-04-30T08:15:22Z"
}
],
"total": 1234,
"page": 1,
"limit": 50
}
Notas:
- Score e calculado em-app a partir de cada linha — para tenants com >100k contatos, considere o filtro do
min_score/max_scorepara reduzir o conjunto antes de paginar. - Ordenacao primaria:
last_inbound_at DESC(contatos mais recentes primeiro).
POST/v1/admin/instances/{instanceId}/temperature/backfill (superadmin)#
Enfileira um task Asynq que bootstraps contact_temperatures para todos os contatos conhecidos da messages table do instance. Idempotente — re-rodar e no-op para linhas já bootstrapped.
Auth: Superadmin
Resposta 202:
{
"status": "queued",
"task_id": "abc123-..."
}
Notas:
- Operação caminha a tabela
messagesinteira para o instance — pode levar segundos a minutos em tenants busy. - Sem endpoint de progresso em v1; verifique o crescimento de linhas em
contact_temperaturesou o estado do task via/v1/admin/queue. - Padrão util: dispare logo após uma pareation +
history.syncpara popular temperatura de todos os contatos conhecidos em batch (em vez de esperar bootstrap lazy no primeiro outbound a cada um).
GET/v1/instances/{instanceId}/profile/avatar#
Retorna a foto de perfil da própria instância conectada. O fluxo é identico ao avatar de contato: a Catcher busca a imagem via worker/whatsmeow, armazena no S3/R2 e expõe apenas URL/local proxy controlado pela plataforma.
Auth: Todos autenticados (JWT ou API Key)
Headers de resposta:
Content-Type: image/jpeg(ou outro tipo conforme a imagem)Cache-Control: public, max-age=86400
Resposta 200: Imagem binaria (JPEG/PNG).
Resposta 404: Instância sem foto de perfil.
Resposta 503: Storage S3/R2 não configurado.
POST/v1/instances/{instanceId}/contacts/{contactId}/presence/subscribe#
Inicia ou renova o monitoramento de presença de um contato 1:1.
Auth: Todos autenticados
Aceita telefone ou JID. Números BR com 9 extra são normalizados automaticamente. Não aceita grupos (
@g.us).
Use este endpoint ao abrir a tela do chat. Ele renova uma lease transiente de monitoramento por 300s. Chamadas repetidas durante a lease são idempotentes e apenas renovam o TTL local.
Resposta 200:
{
"instance_id": "84c2e480-...",
"contact_jid": "551199999999@s.whatsapp.net",
"subscribed": true,
"monitoring_ttl_seconds": 300
}
GET/v1/instances/{instanceId}/contacts/{contactId}/presence#
Retorna o snapshot transiente de presença/typing para um contato 1:1.
Auth: Todos autenticados
O snapshot e mantido em Redis, não em MySQL.
monitoring_active=trueindica que a lease de monitoramento ainda está vigente. O último snapshot conhecido fica em cache por até 24h.typing_stateexpira após 10s sem novos eventos.
Resposta 200:
{
"contact_jid": "551199999999@s.whatsapp.net",
"monitoring_active": true,
"online": true,
"last_seen": 1741360000,
"typing_state": "composing",
"typing_chat": "551199999999@s.whatsapp.net",
"is_group": false,
"observed_at": "2026-03-25T15:04:05Z"
}
| Campo | Tipo | Descricao |
|---|---|---|
contact_jid |
string | JID canonico do contato |
monitoring_active |
bool | Lease de monitoramento ainda ativa |
online |
bool | Último status online/offline conhecido |
last_seen |
int64 | Último visto (unix, pode ser 0 se oculto) |
typing_state |
string | composing, paused ou recording enquanto estiver fresco |
typing_chat |
string | Chat onde o typing foi observado |
is_group |
bool | Sempre omitido/falso para snapshot de contato 1:1 |
observed_at |
string(datetime) | Quando o último evento relevante foi observado |
POST/v1/instances/{instanceId}/contacts/check#
Verifica se números de telefone possuem WhatsApp.
Auth: Todos autenticados
Request:
{
"numbers": ["554137984905", "5541988887777"]
}
Resposta 200:
{
"instance_id": "84c2e480-...",
"results": [
{"query": "554137984905", "jid": "554137984905@s.whatsapp.net", "is_in": true, "verified_name": ""},
{"query": "5541988887777", "jid": "", "is_in": false, "verified_name": ""}
]
}
POST/v1/instances/{instanceId}/contacts/block#
Bloqueia um contato.
Auth: Todos autenticados
Request:
{
"contact_id": "554137984905"
}
Aceita telefone ou JID. Números BR com 9 extra são normalizados automaticamente.
Resposta 200:
{
"instance_id": "84c2e480-...",
"contact_id": "554137984905@s.whatsapp.net",
"blocked": true
}
POST/v1/instances/{instanceId}/contacts/unblock#
Desbloqueia um contato.
Auth: Todos autenticados
Request:
{
"contact_id": "554137984905"
}
Resposta 200:
{
"instance_id": "84c2e480-...",
"contact_id": "554137984905@s.whatsapp.net",
"blocked": false
}