Contatos e consentimento
Todo número que conversa com a sua conta vira um contato. A API deixa o seu sistema — um CRM, uma loja, um ERP — manter essa base em sincronia: criar e atualizar contatos, guardar campos próprios, organizar com tags e, principalmente, registrar quem aceitou receber marketing. É esse registro que decide quem entra numa campanha.
Criar ou atualizar um contato
curl -X POST 'https://api.joinotify.com/contacts?upsert=true' \
-H 'Authorization: Bearer sk_live_xxx' \
-H 'Content-Type: application/json' \
-d '{
"phone": "+55 41 98711-1527",
"firstName": "Ana",
"email": "[email protected]",
"attributes": { "plano_contratado": "Pro" }
}'
O telefone é normalizado para E.164 e guardado sem o + (5541987111527). Sem código
de país, mande defaultCountry: "BR". Números brasileiros com e sem o nono dígito são
reconhecidos como o mesmo contato.
Sem ?upsert=true, um telefone que já existe responde 409 contact_exists com o
contactId dele. Com ele, o contato existente é atualizado e a resposta é 200.
Sincronizar em lote
Para uma base grande, POST /contacts/batch aceita até 500 contatos por chamada e devolve
o resultado de cada linha, na ordem em que foram enviadas:
{
"contacts": [
{ "phone": "+5541987111527", "name": "Ana Souza", "tags": ["cliente"] },
{ "phone": "+5511912345678", "firstName": "Bruno", "attributes": { "cidade": "São Paulo" } }
],
"options": { "onDuplicate": "update", "defaultCountry": "BR" }
}
A resposta é 200 mesmo quando linhas falham — confira results[].status. Linhas com
failed trazem reason e, quando dá, o field culpado. Um telefone repetido dentro do
mesmo lote vira um contato só.
Numa atualização por lote, só valores preenchidos sobrescrevem e as tags são somadas,
nunca tiradas. Para uma importação de milhares de linhas em vários lotes, abra antes um
registro em POST /contacts/imports e mande o id dele em options.importId: os
contadores se somam lá, e o painel mostra o andamento.
contact.created e contact.opted_in saem na criação individual, não no lote — uma
importação de dez mil linhas não vira dez mil chamadas no seu endpoint.
Campos personalizados e tags
Campos personalizados guardam o que é seu: plano contratado, cidade, data da última
compra. Crie o campo uma vez em POST /contacts/fields e grave o valor em attributes,
pela key. O valor é validado pelo tipo — um campo date aceita 2026-09-19 ou
19/09/2026, um boolean aceita sim/não, um select só aceita as opções cadastradas.
Tags são rótulos livres para segmentar. Numa edição individual, tagIds é o conjunto
completo; para pôr ou tirar uma tag de muitos contatos, use POST /contacts/tags/apply
com uma lista de ids ou com os mesmos filtros da listagem.
Consentimento de marketing
Campanhas de marketing só vão para quem aceitou recebê-las. Cada contato tem
optInStatus: opted_in, opted_out ou unknown.
Registre o aceite junto com a evidência de como ele foi obtido — é o que você vai precisar mostrar se alguém perguntar:
curl -X POST https://api.joinotify.com/contacts/cm1c0ntact0001/opt-in \
-H 'Authorization: Bearer sk_live_xxx' \
-H 'Content-Type: application/json' \
-d '{ "evidence": "Checkbox marcado no checkout do pedido 1042" }'
Ou já na criação, com "optIn": { "evidence": "..." }.
O opt-out acontece por vários caminhos, e nem todos passam pela sua integração: a pessoa
manda uma palavra de saída, a Meta recusa um envio com o erro 131050, alguém registra no
painel ou a sua integração chama POST /contacts/{id}/opt-out. Para ficar sabendo de
todos, assine o evento contact.opted_out.
Nenhum caminho automático — lote, upsert, união de duplicados — transforma um opt-out em
opt-in. Só um novo POST /contacts/{id}/opt-in, com evidência, faz isso.
Lista de supressão
A lista de supressão é o que garante o opt-out: telefones e BSUIDs nela nunca recebem
campanha de marketing, seja qual for o consentimento do contato. Um opt-out põe o contato
na lista automaticamente, com o motivo opt_out; você pode adicionar outros à mão em
POST /suppressions.
| Motivo | De onde vem |
|---|---|
opt_out | O contato saiu |
meta_marketing_block | A Meta está bloqueando marketing para esse número |
invalid_number | O número não tem WhatsApp |
manual | Alguém adicionou pelo painel ou por POST /suppressions |
Tirar da lista uma linha opt_out ou meta_marketing_block exige confirm=true — e não
muda o consentimento: o contato continua em opt-out até um novo opt-in.
Privacidade
DELETE /contacts/{id}apaga o contato de vez, com conversas e histórico — é o pedido de eliminação da LGPD. A linha da lista de supressão fica, sem a identidade.GET /contacts/{id}/exportdevolve tudo o que a conta guarda sobre a pessoa, num JSON — o pedido de acesso.GET /contacts/exportgera um CSV da base inteira, ou do que os filtros selecionarem.
Uma API key restrita a alguns números enxerga só os contatos que têm conversa nesses
números: a lista traz só eles, e um contato fora desse recorte responde 404, como um que
não existe. Com ela dá para ler, editar campos e tags e registrar um opt-out.
O que é da base inteira responde 403: exportar, importar, criar, lote, apagar, unir,
opt-in, as definições de campos e tags e a lista de supressão. Para isso, use uma chave
sem restrição.