Contatos

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:

json
{
  "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:

json
{
  "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=true e a instância está conectada, o endpoint enriquece a resposta com dados atualizados do WhatsApp (status, nomes, avatar). Quando live=false ou a instância está desconectada, retorna so o que o Catcher persistiu — resposta sempre disponível.

Nota (politica de proxy — lesson #29): picture_url retorna 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:

json
{
  "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 filtros tier, mutual e search sã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_url retornado nos endpoints GET /chats, GET /contacts, ou GET /contacts/{id} como src de tags <img>. Estas URLs nunca expiram (diferente das URLs do CDN do WhatsApp). Se S3_PUBLIC_URL estiver configurado no servidor, a URL e pública e pode ser usada diretamente; caso contrario, a URL aponta para o endpoint proxy /avatar que 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:

json
{
  "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 contactId aceita 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.IsIn flag do whatsmeow (a contraparte do is_in_address_book da 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=2
  • warm (20-39) — max_consecutive=3
  • engaged (40-59) — max_consecutive=4
  • hot (60-79) — max_consecutive=5
  • very_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:

json
{
  "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:

  • phone aceita 5511999999999, +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:

json
{
  "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_score para 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:

json
{
  "status": "queued",
  "task_id": "abc123-..."
}

Notas:

  • Operação caminha a tabela messages inteira para o instance — pode levar segundos a minutos em tenants busy.
  • Sem endpoint de progresso em v1; verifique o crescimento de linhas em contact_temperatures ou o estado do task via /v1/admin/queue.
  • Padrão util: dispare logo após uma pareation + history.sync para 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:

json
{
  "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=true indica que a lease de monitoramento ainda está vigente. O último snapshot conhecido fica em cache por até 24h. typing_state expira após 10s sem novos eventos.

Resposta 200:

json
{
  "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:

json
{
  "numbers": ["554137984905", "5541988887777"]
}

Resposta 200:

json
{
  "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:

json
{
  "contact_id": "554137984905"
}

Aceita telefone ou JID. Números BR com 9 extra são normalizados automaticamente.

Resposta 200:

json
{
  "instance_id": "84c2e480-...",
  "contact_id": "554137984905@s.whatsapp.net",
  "blocked": true
}

POST/v1/instances/{instanceId}/contacts/unblock#

Desbloqueia um contato.

Auth: Todos autenticados

Request:

json
{
  "contact_id": "554137984905"
}

Resposta 200:

json
{
  "instance_id": "84c2e480-...",
  "contact_id": "554137984905@s.whatsapp.net",
  "blocked": false
}