Contatos
Contatos globais à equipe, com estado de inscrição e propriedades personalizadas.
Contatos são globais à equipe: uma lista por equipe, uma linha por endereço
de e-mail (único, sem diferenciar maiúsculas). Não existe o conceito de
"audiences" — um contato pertence diretamente à sua equipe, 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 e-mail (qualquer capitalização) retorna409, e criar um contato em um endereço cujo domínio nunca recebe e-mail, comoexample.comou.test, retorna422(veja Erros). Um contato que já existe num endereço assim continua lá e ainda pode ser atualizado.first_name,last_nameunsubscribed— o estado global de inscrição. Contatos descadastrados são excluídos de toda transmissão.properties— um mapa plano de valores string personalizados (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.
Tipos de propriedade
Defina uma propriedade para dar um tipo à chave, no painel em
Audiência → Propriedades ou com POST /contact-properties. O tipo é
string, number ou date. string e number funcionam como no Resend, e
date é uma extensão do MillionSend. Uma propriedade de data aceita:
- Uma data ISO 8601, como
2026-01-08, lida como meia-noite UTC. - Uma data e hora ISO 8601, como
2026-01-08T10:30:00Zou2026-01-08 10:30, lida em UTC quando não indica fuso horário. - Um timestamp Unix em segundos ou milissegundos, como
1767225600, entre os anos 2000 e 2100.
Um valor de contato que não combina com o tipo da propriedade é recusado com
422, e um fallback_value que não combina também. As chaves valem com
qualquer capitalização. A data fica guardada como foi escrita, sem os espaços
em volta. GET /contacts/{id} devolve cada valor como {type, value}, então
uma data volta como { "type": "date", "value": "2026-01-08" }. Um valor
guardado antes de a chave ganhar um tipo, e que não combina com ele, volta
como string.
Você ainda pode guardar datas numa propriedade string. Os segmentos e o
aquecimento leem datas de
qualquer propriedade. O tipo date confere cada gravação, pela API ou pelo
painel, e o aquecimento mostra as propriedades de data primeiro.
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 e-mail; a comparação de e-mail 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 e-mail 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 e-mail 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 da equipe; 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": ["lista-antiga-1@example.com", "lista-antiga-2@example.com"] }'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 e-mail
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": "ana@example.com", "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,
"statusCode": 422,
"name": "validation_error",
"message": "email: Invalid email address"
}
]
}ana@example.com só funciona aqui porque o contato já existe: um contato novo
em um endereço reservado como esse é recusado.
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. Cada entrada de
errors traz o statusCode e o name que o modo estrito retornaria para
esse item.
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. Linhas com um endereço que nunca recebe e-mail são puladas, como as de endereço inválido.
Descadastros
E-mails de transmissão 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 transmissões; envios transacionais sem topic_id continuam
chegando — veja Supressões.