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.respondeddas 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" }
]
}| Campo | Regras |
|---|---|
wa_id | Obrigatório: BSUID ou dígitos E.164. Sem ele, 422 missing_wa_id. |
phone, user_id | Opcionais. Quando ausentes, são derivados do wa_id (telefone ou BSUID). |
profile_name, username, parent_user_id | Opcionais, até 200 caracteres. O @ inicial do username é removido. |
customer_id | Restringe o Número aos do Cliente. Cliente de outra Conta retorna 422 invalid_customer. |
phone_number_id ou channel_account_id | Número de destino: ID Meta do Número ou UUID da Conta de canal do WhatsApp. Enviar os dois retorna 422 invalid_request. |
metadata | Campos personalizados, como no PATCH, sem valores null. |
tags | Lista de tags. Não use junto de metadata.tags. |
consents | Lista 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âmetro | Efeito |
|---|---|
customer_id | Contatos das Contas de canal do Cliente; Cliente de outra Conta retorna 422 invalid_customer. |
has_customer | Todo contato pertence a um Cliente: false retorna lista vazia e true não filtra. |
channel, channel_account_id | Canal ou Conta de canal específica. |
profile_name_contains, wa_id_contains | Trecho do nome ou do wa_id, sem diferenciar maiúsculas. |
created_after, created_before | Data de criação, inclusive, em ISO 8601. |
consent | finalidade: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"] }tagssubstitui a lista inteira;[]limpa.add_tagseremove_tagsalteram a lista atual numa única operação, sem perder mudanças simultâneas.- Não combine
tagscomadd_tags/remove_tags, nem commetadata.tagsno 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
metadatasem apagar os que já estavam lá. O nome da planilha vai paradisplay_name(célula vazia mantém o que já havia) e oprofile_namedo 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.
