/contacts

Endpoint pra criar, atualizar ou ler contatos da sua organização.

  • POST /contacts — cria ou atualiza (upsert).
  • GET /contacts — lista paginada, com filtros.
  • GET /contacts?email=... — busca um contato individual por id, email, phone ou cpf.
  • GET /contacts/{id} — um contato.
  • GET /contacts/{id}/conversations — conversas daquele contato.
  • GET /contacts/{id}/history — mudanças de etapa, de dono e notas, numa linha do tempo.
  • PATCH /contacts/{id} — altera nome, email, telefone, notas e campos.
  • DELETE /contacts/{id} — anonimiza (padrão) ou apaga de vez.
  • POST /contacts/{id}/merge — funde outro contato neste.
  • POST /contacts/{id}/opt-out e /opt-in.
  • GET e POST /contacts/{id}/notes — notas do contato.
  • GET /contacts/{id}/export — tudo que existe sobre a pessoa.
  • POST /contacts/batch — até 100 upserts numa chamada.

Auth: Authorization: Bearer wevi_SUA_CHAVE (crie em Conta → Desenvolvedores → API Keys).

Escopos: contacts:read para ler, contacts:write para gravar. Escrever exige plano com o recurso de API; ler funciona em qualquer plano.


POST /contacts

Cria ou atualiza um contato. Se já existe (matched por email ou phone), atualiza. Caso contrário, cria.

Campo Valor
URL https://api.wevi.chat/functions/v1/contacts
Método POST
Auth Authorization: Bearer wevi_SUA_CHAVE
Content-Type application/json
Rate limit 300 req/min por token

Body

Campo Tipo Obrigatório Descrição
email string sim, se phone não vier Email do contato. Lowercase, normalizado.
phone string sim, se email não vier Telefone no formato internacional (ex: +5511999999999).
name string não Nome completo. Vai pra display_name do contato. Sobrescreve sempre que enviado.
fields object não Campos personalizados (chave → valor string). Veja regras abaixo.
tags array de strings não Tags por nome. Se não existir na org, é criada com cor padrão (verde Wevi).
assigned_user_email string não Email do atendente que vira dono da carteira desse contato. Precisa ser membro da org com role admin, editor ou atendente.

Matching: como sabemos se já existe

  1. Procuramos primeiro por email em contact_identifiers.
  2. Se não achar, procuramos por phone.
  3. Se não achar nenhum, criamos contato novo.

Quer matching por CPF ou outro identificador? Use a UI pra cadastrar identificadores e os contatos passam a ser localizáveis também por eles em buscas internas (não via API ainda).

Sobre fields (campos personalizados)

  • Cada chave precisa estar previamente cadastrada na sua org em Conta → Campos personalizados.
  • Valores são salvos como string (mesmo que você mande número ou boolean).
  • Merge: campos existentes do contato não são apagados. Você sobrescreve apenas o que mandar.
  • Se mandar uma chave não cadastrada, a resposta é 400 com unknown_fields: [...].

Pra descobrir quais chaves existem na sua org, use `GET /contacts-schema`.

Sobre tags

  • Array de strings. Vazio ou ausente = não mexe nas tags do contato.
  • Case-insensitive: "VIP" e "vip" são consideradas a mesma tag.
  • Se a tag não existe na org, é criada automaticamente com cor verde.
  • Atribuição não sobrescreve: tags antigas continuam. A API hoje só adiciona.

Exemplos

Mínimo: só email

curl -X POST https://api.wevi.chat/functions/v1/contacts \
  -H "Authorization: Bearer wevi_SUA_CHAVE" \
  -H "Content-Type: application/json" \
  -d '{"email":"joao@exemplo.com"}'

Completo: nome, telefone, campos, tags e dono

curl -X POST https://api.wevi.chat/functions/v1/contacts \
  -H "Authorization: Bearer wevi_SUA_CHAVE" \
  -H "Content-Type: application/json" \
  -d '{
    "email": "joao@exemplo.com",
    "phone": "+5511999999999",
    "name": "João Silva",
    "fields": {
      "cpf": "12345678900",
      "plano_atual": "Pro"
    },
    "tags": ["VIP", "Trial expirando"],
    "assigned_user_email": "maria@suaempresa.com.br"
  }'

Resposta

Sucesso (200)

{
  "ok": true,
  "contact_id": "a1b2c3d4-...",
  "tags_assigned": [
    { "name": "VIP", "id": "uuid-1", "created": false },
    { "name": "Trial expirando", "id": "uuid-2", "created": true }
  ],
  "assigned_user_id": "user-uuid"
}
  • tags_assigned indica quais tags foram aplicadas e se foram criadas agora (created: true) ou já existiam.
  • assigned_user_id aparece se o assigned_user_email foi resolvido com sucesso.

Sucesso com falha parcial de atribuição (200)

Se o assigned_user_email não corresponder a um membro válido, o contato é criado/atualizado normalmente mas vem assignment_error:

{
  "ok": true,
  "contact_id": "a1b2c3d4-...",
  "tags_assigned": [],
  "assignment_error": "assigned_user_email_not_found"
}

Possíveis valores de assignment_error:

  • assigned_user_email_not_found — email não bate com nenhum usuário cadastrado.
  • assigned_user_not_member — usuário existe mas não é membro da sua org.
  • assigned_user_role_not_eligible — usuário é membro mas tem role financeiro (não elegível pra carteira).

Erros (4xx)

Ver lista completa em códigos de erro. Os mais frequentes aqui:

// 400 — falta identificador
{ "error": "At least one of 'email' or 'phone' is required" }

// 400 — chave não cadastrada
{
  "error": "unknown_fields",
  "message": "Algumas chaves em 'fields' não estão cadastradas em Campos personalizados. Cadastre as chaves antes de enviar.",
  "unknown_fields": ["plano_atual"]
}

// 401
{ "error": "Invalid token" }

// 402 — trial expirado
{ "error": "plan_limit_exceeded", "feature": "subscription" }

// 429
{ "error": "Too many requests", "retry_after": 47 }

Padrões de uso

Sincronizar lead novo do CRM

Toda vez que um lead é criado/atualizado no seu CRM, dispare este webhook. O contato vai aparecer na Wevi com tudo já preenchido.

Atribuir vendedor automaticamente

Use assigned_user_email pra que o atendente "dono da carteira" receba transbordos desse contato com prioridade. Veja carteirização pra entender o conceito.

Atualizar status sem duplicar

Como o matching é por email ou phone, mandar o mesmo payload duas vezes só atualiza. Não duplica contato.


GET /contacts

Busca um contato individual com todos os dados (campos nativos, custom, tags, identificadores, dono da carteira e estatísticas de conversa).

Campo Valor
URL https://api.wevi.chat/functions/v1/contacts
Método GET
Auth Authorization: Bearer wevi_SUA_CHAVE
Rate limit 120 req/min por token

Query params

Passe um dos quatro identificadores (ordem de prioridade: idemailphonecpf):

Param Descrição
id UUID do contato. Match exato.
email Email (case-insensitive). Procura primeiro em identifiers, fallback na coluna nativa.
phone Telefone (preferir formato +5511...). Mesmo fallback.
cpf CPF cadastrado como identifier.

Exemplos

# por email
curl "https://api.wevi.chat/functions/v1/contacts?email=cliente@exemplo.com" \
  -H "Authorization: Bearer wevi_SUA_CHAVE"

# por telefone
curl "https://api.wevi.chat/functions/v1/contacts?phone=%2B5511999999999" \
  -H "Authorization: Bearer wevi_SUA_CHAVE"

# por UUID
curl "https://api.wevi.chat/functions/v1/contacts?id=a1b2c3d4-..." \
  -H "Authorization: Bearer wevi_SUA_CHAVE"

Resposta

Sucesso (200)

{
  "id": "a1b2c3d4-...",
  "name": "João Silva",
  "email": "joao@exemplo.com",
  "phone": "+5511999999999",
  "notes": "Cliente VIP, prefere atendimento por WhatsApp",
  "custom_fields": { "cpf": "12345678900", "plano": "Pro" },
  "tags": [
    { "id": "tag-1", "name": "VIP", "color": "#22D650" }
  ],
  "assigned_user": {
    "email": "maria@empresa.com",
    "name": "Maria Silva",
    "role": "atendente"
  },
  "pipelines": [
    {
      "pipeline_id": "pipe-1",
      "pipeline_name": "Funil de vendas",
      "stage_id": "stage-3",
      "stage_name": "Negociação",
      "entered_stage_at": "2026-05-18T09:12:00Z",
      "lost_at": null,
      "lost_reason": null
    }
  ],
  "identifiers": [
    { "type": "email", "value": "joao@exemplo.com", "label": null },
    { "type": "phone", "value": "+5511999999999", "label": null },
    { "type": "cpf",   "value": "12345678900",     "label": null }
  ],
  "stats": {
    "conversation_count": 12,
    "first_seen_at": "2026-01-15T10:23:00Z",
    "last_seen_at":  "2026-05-22T14:30:00Z",
    "channels": ["whatsapp", "web"]
  },
  "assigned_at": "2026-03-10T...",
  "assignment_source": "manual",
  "created_at": "2026-01-15T...",
  "updated_at": "2026-05-22T..."
}

Erros

Status error
400 Provide one of: id, email, phone, cpf
401 Missing Authorization header ou Invalid token
404 Contact not found
429 Too many requests

pipelines traz uma linha por funil em que o contato está, com a etapa atual e desde quando. Funil em que ele não entrou não aparece na lista. lost_at preenchido significa que ele foi marcado como perdido e continua parado naquela etapa.

Padrão de uso

Use após criar/atualizar via POST pra confirmar o estado final, ou pra puxar dados frescos quando seu CRM precisa exibir o que está na Wevi (last_seen, número de conversas, dono da carteira, etapa no funil).


Operações pontuais

Pra modificar um aspecto específico de um contato sem mandar um upsert completo, use os sub-endpoints abaixo. Todos aceitam um identificador no body (id, email, phone ou cpf) e respondem 404 se o contato não existir.

Auth: Authorization: Bearer wevi_SUA_CHAVE em todos.


GET /contacts — lista

Sem nenhum identificador na query, GET /contacts devolve a lista paginada da organização. Com id, email, phone ou cpf, continua devolvendo um contato só, como sempre fez.

Campo Valor
URL https://api.wevi.chat/functions/v1/contacts
Método GET
Rate limit 120 req/min por chave

Filtros

Parâmetro Exemplo Descrição
q maria Busca por nome, email ou telefone
tag Lead Etiqueta, por nome ou id
pipeline Funil de vendas Funil, por nome ou id
stage Proposta Etapa, por nome ou id
lost true Só perdidos, ou só ativos com false
assigned_to uuid ou none Dono da carteira; none traz os sem dono
opted_out true Só quem pediu para não receber mais

Mais limit, cursor e updated_since, descritos em paginação.

curl "https://api.wevi.chat/functions/v1/contacts?tag=Lead&limit=100" \
  -H "Authorization: Bearer wevi_SUA_CHAVE"

Cada item da lista é o mesmo objeto que o GET individual devolve, com tags, funis, identificadores, dono e estatísticas. Não existe uma versão resumida na lista e uma completa no detalhe: você aprende a forma do contato uma vez.

GET /contacts/{id}/conversations

Devolve as conversas do contato, da mais recente para a mais antiga, no mesmo formato de /conversations.

curl "https://api.wevi.chat/functions/v1/contacts/{id}/conversations" \
  -H "Authorization: Bearer wevi_SUA_CHAVE"

GET /contacts/{id}/history

Junta numa linha do tempo só: mudanças de etapa no funil, trocas de dono da carteira e notas registradas.

{
  "data": [
    {
      "type": "stage_changed",
      "at": "2026-09-05T14:22:00Z",
      "data": {
        "pipeline_id": "c24cde19-...",
        "from_stage_id": "00741eec-...",
        "to_stage_id": "3af8c4a2-...",
        "event": "moved",
        "source": "api",
        "reason": null,
        "changed_by": null
      }
    },
    {
      "type": "assignment_changed",
      "at": "2026-09-04T09:10:00Z",
      "data": { "from_user_id": null, "to_user_id": "ca5dd03e-...", "source": "webhook", "reason": null, "changed_by": null }
    },
    { "type": "note_added", "at": "2026-09-03T16:40:00Z", "data": { "id": "...", "body": "Pediu retorno na semana que vem.", "created_by": "..." } }
  ],
  "next_cursor": null,
  "has_more": false
}

type vale stage_changed, assignment_changed ou note_added. Os últimos 100 de cada tipo, ordenados do mais recente para o mais antigo.


POST /contacts/tag — adicionar tag

Anexa uma tag ao contato. Se a tag não existir na org, é criada com cor verde padrão.

Body:

{ "email": "joao@x.com", "tag": "VIP" }
curl -X POST https://api.wevi.chat/functions/v1/contacts/tag \
  -H "Authorization: Bearer wevi_SUA_CHAVE" \
  -H "Content-Type: application/json" \
  -d '{"email":"joao@x.com","tag":"VIP"}'

Resposta 200:

{
  "ok": true,
  "contact_id": "uuid",
  "tag": { "name": "VIP", "id": "tag-uuid", "created": false }
}

created: true se a tag foi criada agora; false se já existia.

DELETE /contacts/tag — remover tag

Remove a atribuição de uma tag. Não apaga a tag da org, só desassocia do contato. Idempotente: se o contato não tinha a tag, responde removed: false sem erro.

Body:

{ "email": "joao@x.com", "tag": "VIP" }
curl -X DELETE https://api.wevi.chat/functions/v1/contacts/tag \
  -H "Authorization: Bearer wevi_SUA_CHAVE" \
  -H "Content-Type: application/json" \
  -d '{"email":"joao@x.com","tag":"VIP"}'

Resposta 200:

{ "ok": true, "contact_id": "uuid", "removed": true, "tag": { "id": "tag-uuid", "name": "VIP" } }

Ou se a tag não existia na org:

{ "ok": true, "contact_id": "uuid", "removed": false, "reason": "tag_not_found" }

POST /contacts/assign — atribuir dono

Define o atendente "dono da carteira" do contato. O email precisa pertencer a um membro da org com role admin, editor ou atendente.

Body:

{ "email": "joao@x.com", "assigned_user_email": "maria@empresa.com" }
curl -X POST https://api.wevi.chat/functions/v1/contacts/assign \
  -H "Authorization: Bearer wevi_SUA_CHAVE" \
  -H "Content-Type: application/json" \
  -d '{"email":"joao@x.com","assigned_user_email":"maria@empresa.com"}'

Resposta 200:

{ "ok": true, "contact_id": "uuid", "assigned_user_id": "user-uuid" }

422 (atendente inválido): assigned_user_email_not_found, assigned_user_not_member ou assigned_user_role_not_eligible.

DELETE /contacts/assign — desatribuir dono

Limpa o dono da carteira. Não exige nenhum campo além do identificador do contato.

curl -X DELETE https://api.wevi.chat/functions/v1/contacts/assign \
  -H "Authorization: Bearer wevi_SUA_CHAVE" \
  -H "Content-Type: application/json" \
  -d '{"email":"joao@x.com"}'

Resposta 200:

{ "ok": true, "contact_id": "uuid", "unassigned": true }

POST /contacts/note — setar notas

Sobrescreve as notas internas do contato. Mande null ou string vazia pra limpar.

Body:

{ "email": "joao@x.com", "note": "Cliente VIP, prefere atendimento por WhatsApp" }
curl -X POST https://api.wevi.chat/functions/v1/contacts/note \
  -H "Authorization: Bearer wevi_SUA_CHAVE" \
  -H "Content-Type: application/json" \
  -d '{"email":"joao@x.com","note":"Cliente VIP"}'

Resposta 200:

{ "ok": true, "contact_id": "uuid", "note": "Cliente VIP" }

POST /contacts/field — setar/limpar custom field

Atualiza um único campo personalizado. A key precisa estar cadastrada em Conta → Campos personalizados (descubra via `GET /contacts-schema`). Mande value: null pra apagar a chave.

Body:

{ "email": "joao@x.com", "key": "plano", "value": "Pro" }
# setar
curl -X POST https://api.wevi.chat/functions/v1/contacts/field \
  -H "Authorization: Bearer wevi_SUA_CHAVE" \
  -H "Content-Type: application/json" \
  -d '{"email":"joao@x.com","key":"plano","value":"Pro"}'

# limpar
curl -X POST https://api.wevi.chat/functions/v1/contacts/field \
  -H "Authorization: Bearer wevi_SUA_CHAVE" \
  -H "Content-Type: application/json" \
  -d '{"email":"joao@x.com","key":"plano","value":null}'

Resposta 200:

{ "ok": true, "contact_id": "uuid", "key": "plano", "value": "Pro" }

Erros:

  • 400 unknown_field se a chave não está cadastrada.
  • 400 'value' is required (use null to clear) se o body não tem campo value.

POST /contacts/stage — mover de etapa no funil

Move o contato para outra etapa de um funil, ou marca e desmarca como perdido. As etapas ficam em Configurações → Funis; veja Funis e etapas de contato.

Campo Obrigatório Valor
identificador sim id, email, phone ou cpf
pipeline não Id ou nome do funil. Omitido, usa o funil padrão da organização.
stage sim, se não mandar lost Id ou nome da etapa de destino. O nome não diferencia maiúsculas.
lost sim, se não mandar stage true marca como perdido na etapa atual, false reabre.
reason não Motivo da perda, até 200 caracteres. Só vale com lost: true.

Mandar stage em um contato que ainda não está no funil coloca ele lá. Mover um contato que estava perdido reabre ele automaticamente.

Body:

{ "email": "joao@x.com", "stage": "Negociação" }
# mover de etapa (funil padrão)
curl -X POST https://api.wevi.chat/functions/v1/contacts/stage \
  -H "Authorization: Bearer wevi_SUA_CHAVE" \
  -H "Content-Type: application/json" \
  -d '{"email":"joao@x.com","stage":"Negociação"}'

# mover em um funil específico
curl -X POST https://api.wevi.chat/functions/v1/contacts/stage \
  -H "Authorization: Bearer wevi_SUA_CHAVE" \
  -H "Content-Type: application/json" \
  -d '{"phone":"+5511999999999","pipeline":"Pós-venda","stage":"Onboarding"}'

# marcar como perdido
curl -X POST https://api.wevi.chat/functions/v1/contacts/stage \
  -H "Authorization: Bearer wevi_SUA_CHAVE" \
  -H "Content-Type: application/json" \
  -d '{"email":"joao@x.com","lost":true,"reason":"Fechou com concorrente"}'

# reabrir
curl -X POST https://api.wevi.chat/functions/v1/contacts/stage \
  -H "Authorization: Bearer wevi_SUA_CHAVE" \
  -H "Content-Type: application/json" \
  -d '{"email":"joao@x.com","lost":false}'

Resposta 200:

{
  "ok": true,
  "contact_id": "uuid",
  "pipeline": { "id": "pipe-1", "name": "Funil de vendas" },
  "stage": { "id": "stage-3", "name": "Negociação" },
  "lost": false
}

Erros:

Status error Quando
400 Provide 'stage' or 'lost' body sem stage e sem lost
400 unknown_pipeline o funil informado não existe, ou a organização não tem funil padrão
400 unknown_stage a etapa não existe naquele funil
400 not_in_pipeline só com lost: o contato ainda não está no funil. Mande stage para colocar ele lá.
404 Contact not found nenhum contato bate com o identificador

Mudar a etapa por aqui vale como qualquer outra mudança: entra no histórico do contato (marcada como feita pela API) e pode disparar automações com o gatilho Entrou em uma etapa do funil.

Diferença pro POST /contacts em batch

O POST /contacts (upsert) faz tudo de uma vez: cria contato, anexa tags, atribui dono, mescla custom_fields. Use quando você tem o pacote completo de dados.

Os sub-endpoints (/contacts/tag, /contacts/assign, etc) são pra operações individuais sobre contato existente:

  • Webhook do seu CRM disparou "lead foi qualificado" → POST /contacts/tag com "qualificado".
  • Vendedor saiu de férias → POST /contacts/assign com o substituto.
  • Cliente cancelou plano → POST /contacts/field com value: null em plano.
  • Proposta enviada no seu ERP → POST /contacts/stage com "Negociação".

Mais granular, sem precisar buscar o contato primeiro pra montar um payload completo.


PATCH /contacts/{id}

Altera só o que você mandar. O que não vier no corpo fica como está.

curl -X PATCH "https://api.wevi.chat/functions/v1/contacts/{id}" \
  -H "Authorization: Bearer wevi_SUA_CHAVE" \
  -H "Content-Type: application/json" \
  -d '{"name":"Maria Souza","fields":{"plano":"premium"}}'
Campo Efeito
name Troca o nome
email, phone Trocam o valor; mande null para limpar. Telefone é normalizado e vira identificador
notes Troca as notas do cadastro
fields Mescla com os campos que já existem
replace_fields Com true, fields substitui tudo em vez de mesclar

Devolve o contato completo, no mesmo formato do GET.

DELETE /contacts/{id}

Por padrão anonimiza: nome, email, telefone, notas e campos personalizados são apagados, o contato é marcado como opt-out e as conversas continuam existindo como histórico de atendimento, sem o que identifica a pessoa. É o que a LGPD pede sem destruir o registro do que foi conversado.

curl -X DELETE "https://api.wevi.chat/functions/v1/contacts/{id}" \
  -H "Authorization: Bearer wevi_SUA_CHAVE"

Para apagar mesmo, incluindo as conversas:

curl -X DELETE "https://api.wevi.chat/functions/v1/contacts/{id}?mode=erase" \
  -H "Authorization: Bearer wevi_SUA_CHAVE"

mode=erase não tem volta.

GET /contacts/{id}/export

Reúne numa resposta só tudo que a plataforma guarda sobre a pessoa: cadastro, conversas, mensagens, notas e disparos. Serve para responder pedido de portabilidade sem ninguém precisar montar consulta no banco.

{
  "contact": { "id": "...", "name": "Maria Souza", "tags": [], "pipelines": [] },
  "conversations": [{ "id": "...", "channel": "whatsapp", "status": "open" }],
  "messages": [{ "role": "user", "content": "...", "created_at": "..." }],
  "notes": [{ "body": "Pediu retorno em janeiro", "created_at": "..." }],
  "sends": [{ "kind": "template", "template_name": "boas_vindas", "status": "delivered" }],
  "exported_at": "2026-09-08T04:47:00Z"
}

POST /contacts/{id}/merge

Funde outro contato neste. Identificadores e conversas passam para cá, campos vazios são preenchidos com os do outro, e o antigo vira uma marca apontando para este.

curl -X POST "https://api.wevi.chat/functions/v1/contacts/{id}/merge" \
  -H "Authorization: Bearer wevi_SUA_CHAVE" \
  -H "Content-Type: application/json" \
  -d '{"contact_id":"id-do-contato-duplicado"}'

POST /contacts/{id}/opt-out e /opt-in

curl -X POST "https://api.wevi.chat/functions/v1/contacts/{id}/opt-out" \
  -H "Authorization: Bearer wevi_SUA_CHAVE"

Quem está em opt-out não recebe template nem abordagem. Responder continua valendo enquanto a janela de 24 horas estiver aberta.

Notas

curl -X POST "https://api.wevi.chat/functions/v1/contacts/{id}/notes" \
  -H "Authorization: Bearer wevi_SUA_CHAVE" \
  -H "Content-Type: application/json" \
  -d '{"body":"Ligou pedindo retorno na semana que vem."}'

GET na mesma rota devolve as notas, da mais recente para a mais antiga. São as mesmas que aparecem na ficha do contato no painel.

POST /contacts/batch

Até 100 contatos por chamada, cada um com o mesmo corpo do POST /contacts. Limite de 30 chamadas por minuto.

curl -X POST "https://api.wevi.chat/functions/v1/contacts/batch" \
  -H "Authorization: Bearer wevi_SUA_CHAVE" \
  -H "Content-Type: application/json" \
  -d '{
    "contacts": [
      { "email": "maria@exemplo.com", "name": "Maria", "tags": ["Lead"] },
      { "phone": "11955554444", "name": "João", "fields": { "origem": "feira" } }
    ]
  }'

O lote nunca falha inteiro por causa de um item. A resposta diz o que aconteceu com cada linha:

{
  "processed": 3,
  "succeeded": 2,
  "failed": 1,
  "results": [
    { "index": 0, "ok": true, "contact_id": "...", "tags_assigned": [] },
    { "index": 1, "ok": true, "contact_id": "...", "tags_assigned": [] },
    { "index": 2, "ok": false, "error": "missing_identifier", "message": "Informe email ou phone." }
  ]
}

Próximo

/contacts-schema