Contatos
Contatos globais ao time, com estado de inscrição e propriedades customizadas.
Contatos são globais ao time: uma lista por time, uma linha por endereço
de email (único, sem diferenciar maiúsculas). Não existe o conceito de
"audiences" — um contato pertence diretamente ao seu time, e você segmenta
com segmentos e tópicos.
No MillionSend Cloud o plano Free guarda até 1.000 contatos (criar um além
disso retorna 403 plan_limit_reached; contatos existentes continuam sendo
atualizados); planos pagos não têm limite de contatos.
Se você está migrando do Resend: os métodos de contato do SDK do Resend
funcionam contra o MillionSend sempre que audienceId for omitido — os
caminhos de contato são os mesmos, sem o aninhamento de audience.
O que um contato armazena
email— a identidade. Criar um segundo contato com o mesmo email (qualquer capitalização) retorna409.first_name,last_nameunsubscribed— o estado global de inscrição. Contatos descadastrados são excluídos de todo broadcast.properties— um mapa plano de valores string customizados (plan: "pro",city: "Berlin"). Objetos aninhados e arrays são rejeitados com422. As propriedades alimentam os campos de mesclagem de templates e os filtros de segmentos.PATCH /contacts/{id}mesclapropertieschave a chave; um valornullremove a chave. O mapa armazenado tem no máximo 100 chaves, nenhuma vazia.
API
Contatos são gerenciados via POST/GET/PATCH/DELETE /contacts e
GET /contacts/{id} — veja a referência da API. O
segmento de caminho {id} aceita tanto o UUID do contato quanto seu endereço
de email; a comparação de email não diferencia maiúsculas.
Duas extensões do MillionSend tornam barata a leitura de uma audiência.
GET /contacts e GET /segments/{id}/contacts aceitam
include=properties,topics e anexam a cada item o mapa de propriedades
{type, value} e as linhas de tópicos que GET /contacts/{id} e
GET /contacts/{id}/topics retornam; sem include, os itens mantêm o formato
do Resend. POST /contacts/batch/get lê até 1.000 contatos por id ou email em
uma requisição, na ordem do pedido, com o mesmo include; entradas que não
correspondem a nenhum contato são listadas em missing em vez de falhar a
chamada. Uma chamada conta como uma requisição no limite de taxa.
As inscrições em tópicos são definidas por contato com
PATCH /contacts/{id}/topics, e GET /contacts/{id}/topics as lê de volta com
os padrões aplicados: a subscription efetiva de cada tópico, se foi escolhida
explicitamente e sua visibility (a página hospedada mostra só tópicos
públicos).
POST /contacts/{id}/preferences-link gera a URL da central de preferências
do contato — { "object": "preferences_link", "contact": "<uuid>", "url": "..." }
— a mesma página que os links de descadastro dos e-mails abrem, para que uma
tela de configurações do seu produto leve direto até ela. O link não expira e
permite a quem o tiver alterar as preferências daquele contato, inclusive o
descadastro global, então entregue-o apenas ao contato. Também disponível como
a ferramenta MCP create_contact_preferences_link.
Toda mudança em um contato publica um
evento de webhook:
contact.created, contact.updated, contact.deleted,
contact.unsubscribed, contact.resubscribed, contact.topic_opt_in e
contact.topic_opt_out, cada um com o source que fez a mudança.
Exclusão em massa
POST /contacts/batch/remove exclui até 1.000 contatos em uma requisição, por
ids ou por emails (exatamente um dos dois; a comparação de email não
diferencia maiúsculas), e retorna as linhas de fato excluídas — entradas
desconhecidas são ignoradas. Excluir mantém os e-mails do contato no log, onde
expiram com a janela de retenção do time; erase: true também apaga cada
endereço do histórico de e-mails, dos payloads de eventos e dos logs da API,
igual a DELETE /contacts/{id}?erase=true. O Resend não tem exclusão em
massa; é uma extensão do MillionSend, também exposta como a ferramenta MCP
delete_contacts.
curl -X POST "https://api.millionsend.com/contacts/batch/remove" \
-H "Authorization: Bearer ms_..." \
-H "Content-Type: application/json" \
-d '{ "emails": ["[email protected]", "[email protected]"] }'Criação em lote
POST /contacts/batch recebe um array JSON de 1 a 1000 itens, cada um no
formato de um corpo de POST /contacts, e grava todos em uma única transação
(uma extensão do MillionSend — o Resend só importa contatos via CSV). O
parâmetro de query on_conflict decide o que acontece com um item cujo email
já pertence a um contato, ou que se repete dentro do lote:
error(padrão) — o item falha:409 Contact already existspara um contato existente,422 Duplicate email in batchpara uma repetição.skip— o contato existente (ou a primeira ocorrência) fica intocado e é reportado comstatus: "skipped"e seu id.upsert— o item é mesclado no contato existente:first_nameelast_namesó quando informados,propertieschave a chave (as chaves informadas sobrescrevem),segmentsadicionados,topicsatualizados. Repetições são unificadas em uma única gravação.
Um lote nunca reinscreve ninguém: unsubscribed: true descadastra o contato,
mas unsubscribed: false em um contato já descadastrado é ignorado — isso
continua sendo um PATCH /contacts/{id} explícito. A lista de supressão
também nunca é tocada.
O header x-batch-validation escolhe entre strict (padrão — o primeiro
item inválido rejeita o lote inteiro com o status dele e o prefixo
contacts.{index}: na mensagem, nada é gravado) e permissive (o
subconjunto válido é gravado e as falhas são listadas em errors).
curl -X POST "https://api.millionsend.com/contacts/batch?on_conflict=upsert" \
-H "Authorization: Bearer ms_..." \
-H "Content-Type: application/json" \
-H "x-batch-validation: permissive" \
-d '[
{ "email": "[email protected]", "first_name": "Ana", "properties": { "plan": "pro" } },
{ "email": "nao-e-um-endereco" }
]'{
"data": [{ "object": "contact", "index": 0, "id": "9b2f…", "status": "updated" }],
"counts": { "created": 0, "updated": 1, "skipped": 0, "failed": 1 },
"errors": [{ "index": 1, "message": "email: Invalid email address" }]
}data mantém a ordem da requisição e traz uma entrada por item bem-sucedido
(status created, updated ou skipped); counts sempre soma o tamanho
da requisição; errors só aparece no modo permissivo.
Importação e exportação CSV
O painel importa contatos de CSV (interpretado no cliente e criado em lote) e exporta a lista atual — inclusive visões filtradas por segmento ou tópico — de volta para CSV.
Descadastros
Emails de broadcast levam headers List-Unsubscribe de um clique (RFC 8058)
e uma página de descadastro hospedada. Um destinatário pode se descadastrar
globalmente ou sair de tópicos individuais.
Descadastros globais definem unsubscribed: true no contato e param envios
com tópico e broadcasts; envios transacionais sem topic_id continuam
chegando — veja Supressões.