MillionSend Docs
Conceitos

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) retorna 409.
  • first_name, last_name
  • unsubscribed — 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 com 422. As propriedades alimentam os campos de mesclagem de templates e os filtros de segmentos. PATCH /contacts/{id} mescla properties chave a chave; um valor null remove 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 exists para um contato existente, 422 Duplicate email in batch para uma repetição.
  • skip — o contato existente (ou a primeira ocorrência) fica intocado e é reportado com status: "skipped" e seu id.
  • upsert — o item é mesclado no contato existente: first_name e last_name só quando informados, properties chave a chave (as chaves informadas sobrescrevem), segments adicionados, topics atualizados. 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.

Nesta página