MillionSend Docs
Conceitos

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) retorna 409, e criar um contato em um endereço cujo domínio nunca recebe e-mail, como example.com ou .test, retorna 422 (veja Erros). Um contato que já existe num endereço assim continua lá e ainda pode ser atualizado.
  • first_name, last_name
  • unsubscribed — 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 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.

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:00Z ou 2026-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 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": "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.

Nesta página