Contatos e funil

Cada Contato do WhatsApp vive numa Etapa do Funil de um Cliente. Negócios diferentes da mesma Conta têm Funis independentes.

Todo Cliente nasce com quatro Etapas padrão, nesta ordem: Não respondeu, Respondeu, Negociando e Concluído. Cada etapa tem um identificador de sistema estável (key): nao_respondeu, respondeu, negociando e concluido.

Classificação automática

Duas transições acontecem sozinhas, sem você precisar mexer:

  • Quando um contato é incluído numa transmissão, ele entra em Não respondeu (se ainda não tinha etapa).
  • Quando ele responde, sobe para Respondeu. Uma resposta conta quando cita o envio (resposta citada ou botão) ou quando chega até 7 dias depois do disparo. Isso é o mesmo sinal que alimenta o contador counters.responded das transmissões.

A classificação automática é sticky ao movimento manual: assim que você (ou uma chamada de API) move um contato de etapa, o automático para de mexer nele. A intenção humana sempre vence.

Criar um contato

POST /api/v1/contacts exige contacts:write e chave live. Só wa_id é obrigatório; a resposta é 201 com o contato e suas tags.

POST /api/v1/contacts
{
  "wa_id": "5511999990000",
  "profile_name": "Ana Souza",
  "customer_id": "UUID-DO-CLIENTE",
  "tags": ["vip", "indicação"],
  "consents": [
    { "channel": "whatsapp", "purpose": "marketing", "state": "granted", "evidence": "Formulário do site" }
  ]
}
CampoRegras
wa_idObrigatório: BSUID ou dígitos E.164. Sem ele, 422 missing_wa_id.
phone, user_idOpcionais. Quando ausentes, são derivados do wa_id (telefone ou BSUID).
profile_name, username, parent_user_idOpcionais, até 200 caracteres. O @ inicial do username é removido.
customer_idRestringe o Número aos do Cliente. Cliente de outra Conta retorna 422 invalid_customer.
phone_number_id ou channel_account_idNúmero de destino: ID Meta do Número ou UUID da Conta de canal do WhatsApp. Enviar os dois retorna 422 invalid_request.
metadataCampos personalizados, como no PATCH, sem valores null.
tagsLista de tags. Não use junto de metadata.tags.
consentsLista de consentimentos iniciais, com os campos de Consentimento. Gravados com origem api.

Sem phone_number_id nem channel_account_id, o contato vai para o único Número da Conta (ou do Cliente). Sem Número, 422 no_phone; com mais de um, 422 ambiguous_phone. Número ou Conta de canal fora da Conta retornam 422 invalid_phone_number ou 422 invalid_channel_account; Conta de canal do Instagram retorna 422 channel_not_supported. Um contato com o mesmo wa_id no Número, com ou sem o nono dígito brasileiro, retorna 409 contact_exists.

O contato e os consentimentos iniciais são gravados juntos: se algum consentimento falhar, a resposta é 500 e nada fica gravado, nem o contato. Pode repetir o mesmo POST. Se a repetição retornar 409 contact_exists, uma tentativa anterior foi concluída, com os consentimentos, e só a resposta se perdeu.

Listar e buscar

GET /api/v1/contacts exige contacts:read, aceita chave sandbox (restrita ao ambiente) e pagina por cursor com limit (até 100), after e before, do contato mais recente para o mais antigo. Filtros:

ParâmetroEfeito
customer_idContatos das Contas de canal do Cliente; Cliente de outra Conta retorna 422 invalid_customer.
has_customerTodo contato pertence a um Cliente: false retorna lista vazia e true não filtra.
channel, channel_account_idCanal ou Conta de canal específica.
profile_name_contains, wa_id_containsTrecho do nome ou do wa_id, sem diferenciar maiúsculas.
created_after, created_beforeData de criação, inclusive, em ISO 8601.
consentfinalidade:estado, como marketing:revoked, em qualquer canal. Outro formato retorna 422 invalid_request.
metadata[chave]Valor exato de um campo personalizado, comparado como texto.

Em GET, PATCH e DELETE de /api/v1/contacts/{id}, o identificador pode ser o UUID do contato, o wa_id ou o telefone. As rotas de consentimento aceitam só o UUID.

Etapas pela API

Liste as Etapas do Cliente para descobrir o id de cada uma. Passe customer_id na query quando a Conta tiver mais de um Cliente; omitir em uma Conta multi-negócio responde 422 customer_required. Um Contato traz a Etapa atual em stage (null quando ainda não foi classificado).

Listar etapas do funil

Lista as etapas do Funil de um Cliente, ordenadas por `position`. Informe `customer_id` quando a Conta tiver mais de um Cliente; o `id` retornado é o que o PATCH de Contato aceita em `stage_id`.

curl https://botozap.com.br/api/v1/contact-stages \
  -H "X-API-Key: $BOTOZAP_KEY"

Resposta 200

{
  "data": [
    {
      "id": "5b0a9d0e-6e1a-4f3b-8c2d-1a9f6e0b7c11",
      "key": "nao_respondeu",
      "label": "Não respondeu",
      "color": "zinc",
      "position": 1
    },
    {
      "id": "5b0a9d0e-6e1a-4f3b-8c2d-1a9f6e0b7c11",
      "key": "respondeu",
      "label": "Respondeu",
      "color": "green",
      "position": 2
    },
    {
      "id": "5b0a9d0e-6e1a-4f3b-8c2d-1a9f6e0b7c11",
      "key": "negociando",
      "label": "Negociando",
      "color": "amber",
      "position": 3
    },
    {
      "id": "5b0a9d0e-6e1a-4f3b-8c2d-1a9f6e0b7c11",
      "key": "concluido",
      "label": "Concluído",
      "color": "pink",
      "position": 4
    }
  ]
}

Em uma Conta com um único Cliente, `customer_id` pode ser omitido por compatibilidade. Com zero ou múltiplos Clientes, a omissão responde `422 customer_required`.

Listar contatos

Lista os contatos da conta, paginado por cursor. Cada contato traz a etapa do funil em `stage` (`null` quando ainda não foi classificado) os campos personalizados em `metadata` e as tags em `tags` (a mesma lista de `metadata.tags`). Filtre por campo com `metadata[chave]=valor`.

curl https://botozap.com.br/api/v1/contacts?limit=2 \
  -H "X-API-Key: $BOTOZAP_KEY"

Resposta 200

{
  "data": [],
  "paging": {
    "cursors": {
      "before": "MjAyNi0wNy0xNFQxMjowMDowMC4wMDBa",
      "after": "MjAyNi0wNy0xNFQxMjowMDowMC4wMDBa"
    },
    "next": "MjAyNi0wNy0xNFQxMjowMDowMC4wMDBa",
    "previous": "MjAyNi0wNy0xNFQxMjowMDowMC4wMDBa"
  }
}

Mover um contato de etapa

Envie o stage_id de uma Etapa do mesmo Cliente do Contato. Uma Etapa de outra Conta ou de outro Cliente é recusada com 422 invalid_stage. O movimento é registrado como manual e passa a ser sticky.

Enviar stage_id: null limpa a etapa: o contato volta ao estado virgem (sem etapa e sem marca de manual) e sai do funil. Não fica sticky, então o próximo disparo que o inclua o reclassifica automaticamente do zero.

Mover contato de etapa

Move um Contato para outra Etapa do mesmo Cliente. Uma Etapa de outra Conta ou de outro Cliente é recusada com 422 `invalid_stage`. O movimento manual passa a ser sticky.

curl https://botozap.com.br/api/v1/contacts/5b0a9d0e-6e1a-4f3b-8c2d-1a9f6e0b7c11 \
  -X PATCH \
  -H "X-API-Key: $BOTOZAP_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "stage_id": "5b0a9d0e-6e1a-4f3b-8c2d-1a9f6e0b7c11"
}'

Resposta 200

{
  "data": {
    "id": "5b0a9d0e-6e1a-4f3b-8c2d-1a9f6e0b7c11",
    "channel": "whatsapp",
    "channel_account": {
      "id": "5b0a9d0e-6e1a-4f3b-8c2d-1a9f6e0b7c11",
      "channel": "whatsapp",
      "display": "+5511999990000"
    },
    "external_id": "5511999990001",
    "wa_id": "5511999990001",
    "profile_name": "Maria",
    "display_name": "Maria - Loja Centro",
    "phone": "5511999990001",
    "user_id": null,
    "username": null,
    "parent_user_id": null,
    "profile_picture_url": null,
    "phone_number_id": "5b0a9d0e-6e1a-4f3b-8c2d-1a9f6e0b7c11",
    "last_seen_at": "2026-07-14T12:00:00.000Z",
    "created_at": "2026-07-14T12:00:00.000Z",
    "notes": "pediu orçamento",
    "metadata": {
      "contrato": "AB-1024",
      "tags": [
        "vip"
      ]
    },
    "stage": {
      "id": "5b0a9d0e-6e1a-4f3b-8c2d-1a9f6e0b7c11",
      "key": "negociando",
      "label": "Negociando"
    },
    "tags": [
      "vip"
    ]
  }
}

Mudar a etapa exige plano pago, trial vigente ou graça legada do Free; sem isso, 422 `plan_restricted`. `stage_id: null` tira o Contato do funil.

Plano do Funil

Mover contatos e criar, editar, reordenar ou excluir Etapas exigem o Funil no plano da Conta. Sem ele, essas operações retornam 422 plan_restricted; a leitura das Etapas e dos contatos continua liberada, assim como editar nome, username, nota, campos e tags. As rotas de CRM e Radar seguem o mesmo plano, mas respondem 403 feature_unavailable.

Consentimento

Cada Contato tem Consentimento por canal (whatsapp ou instagram) e por Finalidade (utility ou marketing). O estado é granted ou revoked; ausência de registro significa nunca informado.GET /api/v1/contacts/{id}/consents devolve o vigente e o histórico. POST grava com origem api. A listagem aceita ?consent=marketing:revoked. No painel, o cartão do Contato tem o bloco de Consentimento; a importação aceita colunas consentimento_marketing e consentimento_utility (sim/nao). A Palavra de saída (sair, parar, stop, cancelar, descadastrar e as que o dono cadastrar no Cliente) revoga marketing na ingestão, origem keyword, e responde a confirmação uma vez. O composer, com a Janela aberta, envia o pedido com botões; o toque grava origem conversation_button.

GET /api/v1/contacts/{id}/consents retorna { current, history }: current traz o registro vigente de cada canal e Finalidade, e history todos os registros, do mais recente para o mais antigo. Cada registro tem id, channel, purpose, state, origin, recorded_by, evidence e created_at.

POST /api/v1/contacts/{id}/consents
{
  "channel": "whatsapp",
  "purpose": "marketing",
  "state": "revoked",
  "evidence": "Pediu por e-mail em 24/09"
}

channel, purpose e state são obrigatórios; evidence é opcional, até 500 caracteres. A resposta é 201 com o registro criado. Valores inválidos retornam 422 invalid_request; contato inexistente ou identificador que não é UUID, 404 not_found.

Nome do contato

Um contato tem dois nomes. profile_name é o nome do perfil no canal: o WhatsApp ou o Instagram informam e ele é atualizado quando chega uma nova mensagem. display_name é o nome que a sua empresa dá ao contato: você o envia no POST ou no PATCH de contato (até 200 caracteres; null limpa), ele volta no shape do contato e nenhuma mensagem recebida o sobrescreve. No painel, ele é o Nome do contato do cartão e a coluna Nome do contato da importação por CSV; quando existe, é o nome mostrado nas telas.

Nota do contato

Cada contato tem um campo notes livre (visível só para a sua equipe), editável pelo PATCH de contato e retornado no shape do contato. São até 4000 caracteres; acima disso, 422 invalid_notes. O PATCH também aceita profile_name e username (até 200 caracteres). Corpo sem campo editável retorna 422 no_updatable_fields.

Tags

Tags ficam em metadata.tags, como lista, e também voltam no campo tags do contato. São até 20 tags de 1 a 40 caracteres; espaços extras são removidos e repetições que só diferem em maiúsculas são descartadas, mantendo a primeira. A mesma lista é usada pelas réguas e pela ferramenta de tags dos agentes.

PATCH /api/v1/contacts/{id}
{ "add_tags": ["vip"], "remove_tags": ["lead frio"] }

PATCH /api/v1/contacts/{id}
{ "tags": ["cliente", "vip"] }
  • tags substitui a lista inteira; [] limpa.
  • add_tags e remove_tags alteram a lista atual numa única operação, sem perder mudanças simultâneas.
  • Não combine tags com add_tags/remove_tags, nem com metadata.tags no mesmo corpo.
  • Lista inválida ou acima do limite retorna 422 invalid_tags.

Gerenciar etapas

Além das quatro padrão, você pode criar etapas custom, renomear/recolorir/ reordenar qualquer etapa e excluir as custom. As etapas de sistema (key não nulo) não podem ser excluídas, porque a automação depende de nao_respondeu e respondeu. Excluir uma etapa custom devolve os contatos dela ao estado sem etapa.

Em Conta multi-negócio, criação e edição recebem customer_idno corpo; exclusão recebe o mesmo id na query. Para reordenar, envie a ordem completa em PATCH /api/v1/contact-stages/reordercom { customer_id, stage_ids: [...] }. A operação é atômica: lista incompleta, duplicada ou estrangeira não altera posição alguma e retorna 422 invalid_stage_order.

Cada Cliente tem até 50 Etapas; acima disso, a criação retorna 422 stage_limit_reached. O rótulo (label) é obrigatório na criação (422 missing_label) e tem até 60 caracteres. Cor fora da paleta retorna 422 invalid_color, e excluir uma Etapa de sistema, 422 system_stage.

Criar etapa custom

Cria uma Etapa custom no fim do Funil do Cliente. Envie `customer_id` em Conta multi-negócio; `color` é zinc, green, amber ou pink.

curl https://botozap.com.br/api/v1/contact-stages \
  -X POST \
  -H "X-API-Key: $BOTOZAP_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "label": "Perdido",
  "color": "zinc"
}'

Resposta 201

{
  "data": {
    "id": "5b0a9d0e-6e1a-4f3b-8c2d-1a9f6e0b7c11",
    "key": null,
    "label": "Perdido",
    "color": "zinc",
    "position": 5
  }
}

Este exemplo executável assume uma Conta com um único Cliente. Em multi-negócio, inclua `customer_id` no corpo.

Editar etapa

Renomeia ou recolore uma Etapa do Cliente (inclusive as de sistema). Para reordenar o Funil inteiro, use a operação atômica `/contact-stages/reorder`.

curl https://botozap.com.br/api/v1/contact-stages/5b0a9d0e-6e1a-4f3b-8c2d-1a9f6e0b7c11 \
  -X PATCH \
  -H "X-API-Key: $BOTOZAP_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "label": "Em negociação",
  "color": "amber"
}'

Resposta 200

{
  "data": {
    "id": "5b0a9d0e-6e1a-4f3b-8c2d-1a9f6e0b7c11",
    "key": "negociando",
    "label": "Em negociação",
    "color": "amber",
    "position": 3
  }
}

Este exemplo executável assume uma Conta com um único Cliente. Em multi-negócio, inclua `customer_id` no corpo.

Excluir etapa custom

Exclui uma Etapa custom do Cliente; seus Contatos voltam ao estado sem Etapa. Em Conta multi-negócio, envie `customer_id` na query.

curl https://botozap.com.br/api/v1/contact-stages/5b0a9d0e-6e1a-4f3b-8c2d-1a9f6e0b7c11 \
  -X DELETE \
  -H "X-API-Key: $BOTOZAP_KEY"

Resposta 204

Este exemplo executável assume uma Conta com um único Cliente. Em multi-negócio, acrescente `?customer_id=<uuid>`.

Campos personalizados

Além do funil, cada contato guarda um conjunto livre de campos em metadata: pares de chave e valor para você anotar dados do seu próprio sistema. O caso típico é um identificador externo, por exemplo um campo Contrato com o número que aquele contato tem no seu sistema. Cada campo que você cria vira uma coluna na lista de contatos e ganha uma key estável (um slug derivado do nome) que indexa o valor.

Você cria, renomeia, reordena e exclui campos pelo botão Gerenciar campos na lista de contatos, ou pela API. São até 20 campos por conta. Cada campo tem um type: text (padrão, o que as definições antigas continuam sendo), number, boolean ou date. Um campo date grava o valor em metadata como ISO AAAA-MM-DD (por exemplo 2000-02-29); 31/02/2000, 2000-2-1 e texto livre respondem 422 invalid_date_field. No painel o cartão do Contato mostra um seletor de data e a lista exibe DD/MM/AAAA. A key e o type não mudam depois de criados. Excluir um campo remove apenas a coluna da tela: o valor que já estava gravado em cada contato continua lá e segue acessível em metadata pela API.

Listar campos personalizados

Lista as definições de campos personalizados da conta, ordenadas por `position`. A `key` de um campo é a chave usada em `contacts.metadata` e no filtro `metadata[key]=...`. `type` é `text`, `number`, `boolean` ou `date`.

curl https://botozap.com.br/api/v1/contact-fields \
  -H "X-API-Key: $BOTOZAP_KEY"

Resposta 200

{
  "data": []
}

Criar campo personalizado

Cria uma definição de campo. `label` é obrigatório; `key` é opcional (sem ela, derivamos um slug do label). `type` é opcional (`text` por padrão; também `number`, `boolean`, `date`). A `key` indexa `contacts.metadata`.

curl https://botozap.com.br/api/v1/contact-fields \
  -X POST \
  -H "X-API-Key: $BOTOZAP_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "label": "Contrato"
}'

Resposta 201

{
  "data": {
    "id": "5b0a9d0e-6e1a-4f3b-8c2d-1a9f6e0b7c11",
    "key": "contrato",
    "label": "Contrato",
    "type": "text",
    "position": 1,
    "created_at": "2026-07-14T12:00:00.000Z"
  }
}

Editar campo personalizado

Renomeia ou reordena um campo (`label`, `position`). A `key` e o `type` são imutáveis (informá-los responde 422).

curl https://botozap.com.br/api/v1/contact-fields/5b0a9d0e-6e1a-4f3b-8c2d-1a9f6e0b7c11 \
  -X PATCH \
  -H "X-API-Key: $BOTOZAP_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "label": "Nº do contrato"
}'

Resposta 200

{
  "data": {
    "id": "5b0a9d0e-6e1a-4f3b-8c2d-1a9f6e0b7c11",
    "key": "contrato",
    "label": "Nº do contrato",
    "type": "text",
    "position": 1,
    "created_at": "2026-07-14T12:00:00.000Z"
  }
}

Excluir campo personalizado

Remove a definição do campo (a coluna da tela). NÃO apaga o dado já gravado em `contacts.metadata`: a chave segue acessível pela API.

curl https://botozap.com.br/api/v1/contact-fields/5b0a9d0e-6e1a-4f3b-8c2d-1a9f6e0b7c11 \
  -X DELETE \
  -H "X-API-Key: $BOTOZAP_KEY"

Resposta 204

Preencher os valores de um contato

No painel, o lápis no fim de cada linha da lista abre o editor dos campos daquele contato. Deixar um campo vazio remove a chave. Pela API é o mesmo PATCH de contato: envie metadata com as chaves a gravar (o merge é raso, então as chaves que você não enviar ficam intactas) e use null num valor para remover aquela chave. Os valores voltam no shape do contato em metadata, e você pode filtrar a listagem por metadata[chave]=valor.

Importar contatos por CSV

O botão Importar na lista abre o assistente de CSV. A primeira linha do arquivo é o cabeçalho e é obrigatória: ela é a base do mapeamento das colunas.

  • Você indica qual coluna é o Telefone (uma, obrigatória), qual é o Nome do contato, e mapeia as demais para campos que já existem ou cria um campo na hora a partir do nome da coluna.
  • A importação aceita até 2000 linhas por vez; o que passar disso é ignorado com um aviso.
  • Contatos que já existem são atualizados: os campos do CSV entram no metadata sem apagar os que já estavam lá. O nome da planilha vai para display_name (célula vazia mantém o que já havia) e o profile_name do canal não é tocado. A identidade derivada (telefone, BSUID) também não regride.
  • Números brasileiros com e sem o nono dígito são reconhecidos como o mesmo contato, então a planilha não cria uma duplicata quando o WhatsApp já conhece o número na outra forma.

Cada célula vira o valor do tipo do campo: number aceita 42, 3,5 ou 1.234,56; boolean aceita sim/não; date aceita 25/09/2026 ou 2026-09-25 e grava AAAA-MM-DD. A linha inválida aparece no relatório e as demais seguem. Contatos, campos criados na hora e Consentimentos da planilha são gravados numa transação só: se algo falha, nada entra.

Ao final, o assistente mostra quantos contatos foram criados, quantos atualizados e quantos foram ignorados, com o motivo de cada linha inválida (por exemplo, telefone fora do formato).

Excluir um contato

DELETE /api/v1/contacts/{id} exige contacts:write e chave live, e apaga na hora, antes de responder 204: as mensagens do contato, os registros de mídia dessas mensagens, as conversas, os eventos dele no stream de eventos e o próprio contato. Oportunidades, demandas, agendamentos e consentimentos do contato também são removidos. Não há como desfazer.

A exclusão é tudo ou nada: se falhar, a resposta é 500, nada é apagado e você pode repetir. Uma repetição que retorna 404 indica que uma tentativa anterior já apagou o contato. Os arquivos de mídia já baixados não são removidos do armazenamento por esta chamada, só os registros que apontam para eles. Entregas de webhook já enviadas e logs de requisição seguem a própria retenção.