Plumail

Execute um workspace de e-mail Plumail a partir de um agente: assinantes, segmentos, campanhas, envios transacionais, estatísticas de entrega e lista de supressão. Servidor remoto hospedado, hospedagem na UE, chaves de API com escopo.

Servidor MCP hospedado

npx add-mcp 'https://plumail.fr/api/mcp'

Instala no Claude Code, Codex, Cursor e outros

Documentação

A API REST e o servidor MCP Plumail: envio de e-mails, gestão de assinantes e campanhas a partir do seu aplicativo ou de um agente de IA. Referência completa.

O Plumail é controlado de fora: a partir do seu aplicativo, de um script ou de um agente como Claude Code, Codex ou Cursor. Duas portas, a mesma chave.

Para quemEndereço
API RESTPara código — qualquer linguagem sabe fazer uma requisição HTTP.https://mailcheer.com/api/v1
Servidor MCPPara agentes de IA, que descobrem sozinhos as ferramentas disponíveis.https://mailcheer.com/api/mcp

Sua primeira chave

No seu espaço Plumail: Conta → API e agentes de IA → Nova chave. Você dá um nome a ela e marca o que ela tem permissão de fazer.

A chave completa é exibida apenas uma vez. Nós guardamos somente uma impressão digital: se você a perder, ninguém pode devolvê-la — você cria outra e corta a antiga. É o preço a pagar para que um roubo da nossa base não forneça nenhuma chave utilizável, e é o preço certo.

Guarde-a como uma senha: nas variáveis de ambiente do seu serviço, nunca em código compartilhado nem em uma página pública.

Dois tipos de chave: uma chave real (mch_live_…) envia de verdade; uma chave de teste (mch_test_…) verifica tudo e não envia nada — veja O modo de teste.

Os direitos de uma chave

DireitoO que ele abre
emails:sendEnviar e-mails unitários e ler o estado deles.
subscribers:readLer os assinantes e a lista de supressão.
subscribers:writeAdicionar, modificar e cancelar a inscrição de assinantes.
campaigns:readLer as campanhas e suas estatísticas.
campaigns:writeCriar e enviar campanhas, excluir um rascunho.
webhooks:readLer as assinaturas de eventos e seu histórico.
webhooks:writeCriar, modificar e excluir assinaturas de eventos.

Marque apenas o necessário. Uma chamada fora do escopo da chave responde 403, e nada contorna isso — é a única salvaguarda que se sustenta diante de um agente autônomo: não contamos com a prudência dele, tiramos o botão.

Os direitos são escolhidos na criação e não mudam mais. Uma chave cujo escopo pode ser ampliado depois não significa mais nada: quem a recebeu acredita ter acesso de leitura e se vê com o direito de enviar, sem ter sido avisado.

O limite e o idioma de uma chave

Duas configurações de uma chave podem mudar após a criação — Configurações → API, Ajustar —, porque nenhuma das duas dá mais poder a quem a possui.

O limite mensal (opcional): "esta chave não pode ultrapassar N e-mails por mês". Ele conta os e-mails da chave e as campanhas que ela lança (POST /api/v1/campaigns/{id}/send), incluindo cada endereço em cópia, incluindo o que está na fila, no mês UTC — o mês da cota. Ele se soma à cota do espaço, nunca a substitui: impede que um uso consuma o lugar de outro. Um CRM que envia seus códigos de acesso e sua newsletter a partir do mesmo espaço limita a chave da newsletter, e os códigos sempre têm espaço.

Acima disso, o envio é recusado por inteiro antes que qualquer coisa saia — 402 key_quota_exceeded, distinto do quota_exceeded do espaço: outra chave do espaço ainda pode enviar.

{
  "error": {
    "code": "key_quota_exceeded",
    "message": "Plafond de la clé atteint : la clé « Newsletter » ne peut pas envoyer plus de 2 400 e-mails par mois. …",
    "details": {
      "key_id": "cmu8k1a2b00001n7nkey0news",
      "key_name": "Newsletter",
      "limit": 2400,
      "used": 2380,
      "remaining": 20,
      "requested": 551,
      "resets_at": "2026-10-01T00:00:00.000Z"
    }
  },
  "statusCode": 402,
  "message": "…",
  "name": "key_quota_exceeded"
}

O dry_run de uma campanha anuncia isso da mesma forma (would_send: false, blocked_code: "key_quota_exceeded"). Uma chave limitada também carrega três cabeçalhos em cada resposta, ao lado dos Mailcheer-Quota-* do espaço:

CabeçalhoValor
Mailcheer-Key-Quota-LimitO limite mensal da chave.
Mailcheer-Key-Quota-UsedO que a chave enviou neste mês, mais o que ainda está na fila.
Mailcheer-Key-Quota-RemainingO que ela ainda pode enviar; nunca negativo.

O contador da chave reinicia junto com o do espaço (Mailcheer-Quota-Reset), e GET /api/v1/me o devolve em key.monthly_limit. Uma chave de teste não tem limite: ela não envia nada.

O idioma das respostas: inglês (padrão) ou francês — o idioma das mensagens quando uma chamada não solicita nenhum. Veja O idioma das respostas.

Enviar um e-mail

É o ponto de entrada mais usado: a fatura, o alerta, a senha esquecida — tudo o que seu aplicativo escreve para uma pessoa por vez.

curl -X POST https://mailcheer.com/api/v1/emails \
  -H "Authorization: Bearer mch_live_…" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: facture-2026-0412" \
  -d '{
    "from": "Votre marque <bonjour@votredomaine.fr>",
    "to": "client@exemple.fr",
    "subject": "Votre facture de septembre",
    "html": "<p>La voici.</p>"
  }'

A resposta chega em 202:

{
  "id": "cmu651xf200021n7nm68sikfw",
  "object": "email",
  "from": "bonjour@votredomaine.fr",
  "to": ["client@exemple.fr"],
  "subject": "Votre facture de septembre",
  "created_at": "2026-09-18T07:12:44.102Z"
}

202, e não 200: nosso provedor de envio aceitou a mensagem, ela ainda não está em uma caixa de entrada. A entrega é constatada alguns segundos depois:

curl https://mailcheer.com/api/v1/emails/cmu651xf200021n7nm68sikfw \
  -H "Authorization: Bearer mch_live_…"

O campo status passa de sent para delivered, ou para bounced se o endereço não existir, ou para complained se a pessoa marcou a mensagem como indesejada. Nesses dois últimos casos, o endereço entra automaticamente na lista de supressão: seu aplicativo não precisa se preocupar com isso.

Com vários endereços, status segue os destinatários de to: uma cópia (cc, bcc) que retorna com erro não faz o e-mail do seu destinatário passar por bounced. Cada endereço tem seu próprio estado em recipients: email, type (to, cc ou bcc), status (sent enquanto a Amazon não disse nada, depois delivered, bounced ou complained; queued ou failed enquanto o próprio e-mail estiver assim) e reason (o tipo de bounce dado pela Amazon: Permanent, Transient ou Undetermined, null caso contrário). Os webhooks seguem a mesma regra: cada evento email.* nomeia UM endereço em email e diz em recipient_type se é um destinatário (to) ou uma cópia (cc, bcc). A abertura permanece no e-mail (opened_at): o pixel é o mesmo em todas as cópias, ele não diz quem abriu.

O cancelamento de inscrição em um clique

Se você escreve para pessoas que não escreveram para você primeiro — uma newsletter, um alerta ao qual se assina, uma página de status — Gmail e Yahoo esperam um link de cancelamento de inscrição nos cabeçalhos da mensagem, e não apenas no rodapé. É a exigência deles desde fevereiro de 2024.

Forneça o endereço com unsubscribe_url, e o Plumail coloca os dois cabeçalhos que andam juntos:

curl -X POST https://mailcheer.com/api/v1/emails \
  -H "Authorization: Bearer mch_live_…" \
  -H "Content-Type: application/json" \
  -d '{
    "from": "Votre marque <alertes@votredomaine.fr>",
    "to": "client@exemple.fr",
    "subject": "Votre rapport du mois",
    "html": "<p>Le voici.</p>",
    "unsubscribe_url": "https://votredomaine.fr/desinscription/abc123"
  }'

Seu endereço deve aceitar um POST e cancelar a inscrição sem pedir confirmação: é o princípio do "um clique". Um GET no mesmo endereço pode levar a uma página legível, para clientes de e-mail que fazem um ou outro.

Sem esse cabeçalho, a única saída oferecida ao destinatário é o botão "Spam" — e é a reputação do seu domínio de envio que paga por isso, não a da mensagem.

⚠️ List-Unsubscribe permanece recusado em headers: é o Plumail que o escreve, você fornece apenas o alvo. Isso garante que List-Unsubscribe-Post sempre o acompanhe — sem esse segundo cabeçalho, o Gmail não exibe nenhum botão.

unsubscribe_url também diz o que é o envio. Com ele, é uma carta ou prospecção: um endereço com inscrição cancelada no seu espaço é recusado (422 suppressed_recipient). Sem ele, é um e-mail individual — uma resposta, uma fatura, um compromisso — e um cancelamento de inscrição não o impede. Um bounce, uma reclamação ou uma remoção manual interrompem ambos.

Os anexos

Um orçamento, uma fatura, um folheto: forneça-os em attachments, no formato da Resend — filename e content, o arquivo codificado em base64.

curl -X POST https://mailcheer.com/api/v1/emails \
  -H "Authorization: Bearer mch_live_…" \
  -H "Content-Type: application/json" \
  -d '{
    "from": "Votre marque <bonjour@votredomaine.fr>",
    "to": "client@exemple.fr",
    "subject": "Votre devis",
    "text": "Le devis est en pièce jointe.",
    "attachments": [
      {
        "filename": "devis-2026-09.pdf",
        "content": "JVBERi0xLjQKJcfsj6IK…",
        "content_type": "application/pdf"
      }
    ]
  }'

Em Node, o conteúdo se prepara em uma linha:

import { readFileSync } from "node:fs";

const devis = {
  filename: "devis-2026-09.pdf",
  content: readFileSync("./devis-2026-09.pdf").toString("base64"),
};

content_type é opcional: ele é deduzido da extensão (.pdf → application/pdf) e vale application/octet-stream como último recurso. No máximo vinte anexos por mensagem.

O limite é o da Amazon, nosso provedor de envio: 40 MB por mensagem uma vez codificada, ou seja, cerca de 30 MB de arquivos reais — o base64 adiciona um terço. Acima disso, a chamada é recusada em 422, com o nome do arquivo, seu peso e o peso atingido. Isso é deliberado: uma recusa que explica vale mais do que uma mensagem que sai sem seu anexo.

São recusados da mesma forma, e sempre dizendo o motivo:

  • as extensões que os serviços de e-mail rejeitam — .exe, .bat, .js, .vbs, .scr … Coloque o arquivo em um arquivo .zip, ou envie um link de download;
  • um content que não é base64 válido;
  • um campo path, que daria uma URL para buscar: nosso servidor não segue um endereço que você escolhe. Codifique o arquivo.

Quando uma mensagem carrega um anexo, ela sai em MIME completo em vez de mensagem simples. Todo o resto não muda: cc, bcc, reply_to, seus cabeçalhos, o cancelamento de inscrição em um clique e suas etiquetas funcionam de forma idêntica — e o bcc ainda não aparece em nenhum cabeçalho da mensagem recebida.

As imagens incorporadas

Um logotipo, uma foto de produto que devem ser exibidos dentro da mensagem, não como anexo para baixar: dê ao anexo um content_id e chame-o no HTML por cid:.

{
  "html": "<p><img src=\"cid:logo\" alt=\"Votre marque\" width=\"120\"></p><p>Merci pour votre commande.</p>",
  "attachments": [
    { "filename": "logo.png", "content": "iVBORw0KGgo…", "content_type": "image/png", "content_id": "logo" }
  ]
}

O anexo sai com o cabeçalho Content-ID, ao lado do HTML, e os serviços de e-mail o exibem em seu lugar. content_id (ou contentId) aceita letras, números, ., _, - e @, sem espaço; dois anexos não podem ter o mesmo. Os mesmos limites dos outros anexos se aplicam: é um arquivo da mensagem como qualquer outro.

As cópias

cc e bcc aceitam um endereço ou uma matriz de 1 a 50, como to — mas no máximo 50 endereços no total, to, cc e bcc juntos: é o limite do nosso provedor de envio por mensagem. Acima disso, 422 validation_error e nada sai; divida em várias chamadas, ou use uma campanha. Ambos contam na sua cota e passam pelos mesmos controles — um endereço que a lista de supressão recusa faz a chamada inteira falhar, seja ele destinatário ou em cópia.

A medição de aberturas

Um e-mail em HTML carrega uma imagem invisível de um pixel que conta as aberturas. Sem track_opens na requisição, é a configuração Medir aberturas do seu espaço que decide (tela Espaço do aplicativo); track_opens: true ou false prevalece para este e-mail. Um e-mail somente em texto não carrega nenhum pixel. GET /api/v1/emails/{id} torna track_opens: quando vale false, opened_at permanece null porque a abertura não é medida, não porque ninguém abriu.

Na França, a CNIL considera desde sua recomendação de 14 de abril de 2026 que medir aberturas por um pixel, para acompanhar o desempenho das campanhas, exige o consentimento prévio do destinatário. Um código de acesso ou uma redefinição de senha não tem motivo para ser rastreado: envie track_opens: false.

As cinco regras de todo envio

Elas não são contornáveis, e são as mesmas para uma campanha enviada pela interface.

1. from deve estar em um domínio verificado do seu espaço. Caso contrário, 422 unverified_from_domain, com a lista dos seus domínios verificados na mensagem. GET /api/v1/me também os fornece. 2. Um endereço em lista de supressão é recusado, com seu motivo — endereço morto, reclamação, remoção manual e descadastramento quando o envio carrega unsubscribe_url. Um e-mail individual, sem unsubscribe_url, parte para um endereço descadastrado: remover-se da sua newsletter não é recusar a resposta ao próprio pedido. A chamada inteira falha, incluindo os outros destinatários: um envio parcial do qual você não saberia nada é o pior resultado possível, porque você acreditaria ter avisado todo mundo.

3. A cota mensal da sua oferta conta esses envios como as campanhas — e o que já aguarda na fila. É a mesma conta de envio, a mesma fatura. Um envio que não cabe responde 402 quota_exceeded (ver abaixo). Na oferta gratuita, os e-mails oferecidos de um domínio de envio servem a apenas um espaço por mês: em outros lugares, é 402 free_plan_domain_used.

4. Uma taxa de rejeição ou de reclamações muito alta suspende o envio. Os limites são os da Amazon: 5% de rejeições, 0,1% de reclamações. Uma aplicação que escreve para endereços inventados causa os mesmos danos que uma campanha em lista comprada.

5. Nada contorna o double opt-in. Um assinante adicionado pela API recebe uma confirmação, exceto double_opt_in: false explícito — e então é você quem responde pelo consentimento. Um mesmo endereço recebe no máximo uma confirmação a cada 2 minutos e 3 em 24 horas, em todos os canais (formulário, API, MCP): uma nova chamada atualiza o registro sem reenviar e-mail, e a resposta diz o motivo (confirmation_sent: false, confirmation_not_sent_reason, confirmation_retry_at). Além disso, cada novo contato é uma reclamação possível contra o seu domínio.

O modo de teste

Crie uma chave de teste: Configurações → API → Nova chave, interruptor Chave de teste ligado. Ela começa com mch_test_. Com ela, cada envio é verificado exatamente como um real — from em um domínio verificado, destinatários, lista de supressão, status de envio e limite diário do espaço, cota do mês, regra da oferta gratuita por domínio, Idempotency-Key — e recebe a mesma resposta, incluindo recusas. Mas nada parte, e sua cota não é afetada: os cabeçalhos Mailcheer-Quota-* dão os números reais, inalterados.

curl -X POST https://mailcheer.com/api/v1/emails \
  -H "Authorization: Bearer mch_test_…" \
  -H "Content-Type: application/json" \
  -d '{
    "from": "Votre marque <bonjour@votredomaine.fr>",
    "to": "bounced@simulator.mailcheer.com",
    "subject": "Votre facture de septembre",
    "html": "<p>La voici.</p>"
  }'

Mesmo 202, mesmo corpo. Cada resposta autenticada diz qual tipo de chave respondeu — Mailcheer-Mode: test ou Mailcheer-Mode: live — e GET /api/v1/me o retorna em key.mode. Verifique isso uma vez ao iniciar seu serviço: uma chave de teste implantada em produção por engano acredita estar enviando, e nada parte.

O que acontece em seguida

Dois segundos depois, o e-mail recebe seus retornos, como um real: GET /api/v1/emails/{id} passa de sent ao seu desfecho, endereço por endereço, e seus webhooks recebem os eventos — chamadas reais, assinadas, com novas tentativas — marcados "test": true ao lado de type (ver Os eventos do modo de teste).

Todo endereço se comporta como entregue. Para os outros desfechos, escreva para:

EndereçoEventos
delivered@simulator.mailcheer.comemail.delivered
bounced@simulator.mailcheer.comemail.bounced, com reason: "Permanent"
complained@simulator.mailcheer.comemail.delivered, depois email.complained

Uma etiqueta pode acompanhar um + para distinguir seus testes: bounced+inscription@simulator.mailcheer.com. Esses endereços só funcionam com uma chave de teste: uma chave real é recusada com um 422, já que um e-mail real rejeitaria neles.

Uma rejeição ou reclamação simulada não adiciona o endereço à sua lista de supressão: o modo de teste não muda nada de real.

O que uma chave de teste não faz

Uma chave de teste tem apenas um direito, emails:send, e ele é simulado. Ela não lê nem modifica nenhum dado real do espaço — assinantes, campanhas, lista de supressão, webhooks: essas chamadas retornam 403 insufficient_scope, com details.mode: "test". Uma chave de teste acaba em um repositório, integração contínua, exemplo compartilhado: ela não pode divulgar o endereço de ninguém, nem inserir um assinante de teste em uma campanha real. As campanhas já têm seu jeito de testar sem enviar: dry_run e POST /api/v1/audience.

Com uma chave de teste, GET /api/v1/emails/{id} só encontra os e-mails de teste; uma chave real nunca os vê. Os e-mails de teste são mantidos por 30 dias.

Duas diferenças em relação a um envio real, ambas intencionais:

  • a revisão de segurança do conteúdo, que os primeiros envios de um espaço novo passam, não ocorre: nada parte, não há o que proteger;
  • no máximo 1.000 endereços de teste por 24 horas e por espaço, to, cc e bcc incluídos. Acima disso: 429 rate_limit_exceeded, com Retry-After.

O servidor MCP segue a chave: com uma chave de teste, send_email é verificado e simulado, e get_account retorna mode: "test".

Onde está o seu espaço

GET /api/v1/me é a primeira chamada a fazer, e o bom reflexo antes de um envio importante: ela diz a quem pertence a chave, com quais endereços escrever — e se o envio vai caber. Nenhum direito especial é exigido.

curl https://mailcheer.com/api/v1/me -H "Authorization: Bearer mch_live_…"
{
  "object": "account",
  "organization": { "id": "org_3f9", "name": "Votre marque", "slug": "votre-marque" },
  "key": {
    "id": "cmu8k1a2b00001n7nkey0prod",
    "name": "Production",
    "scopes": ["emails:send", "subscribers:read"],
    "mode": "live",
    "language": "en",
    "monthly_limit": null
  },
  "plan": { "id": "free", "name": "Découverte", "emails_per_month": 3000 },
  "usage": {
    "period": "2026-09",
    "emails_sent": 2410,
    "emails_in_flight": 120,
    "emails_remaining": 470,
    "resets_at": "2026-10-01T00:00:00.000Z"
  },
  "billing": {
    "status": "none",
    "subscribed_plan": null,
    "current_period_end": null,
    "cancel_at_period_end": false,
    "trial_ends_at": null,
    "scheduled_change": null,
    "manage_url": "https://mailcheer.com/fr/reglages/facturation"
  },
  "limits": {
    "members": { "used": 1, "pending_invitations": 0, "max": 1 },
    "sending_domains": { "used": 1, "max": 1 },
    "daily": null
  },
  "subscribers": 551,
  "sending_domains": [{ "domain": "votredomaine.fr", "verified": true }],
  "senders": [{ "id": "snd_71a", "from": "bonjour@votredomaine.fr", "name": "Votre marque", "default": true }]
}
  • key — a chave que fez a chamada: seus scopes, seu mode (live, ou test para uma chave de teste), sua language (o idioma de suas mensagens quando uma chamada não pede nenhum) e seu monthly_limit — { "limit", "used", "remaining", "resets_at" }, ou null sem limite.
  • usage — emails_sent: o que partiu neste mês, em todos os canais. emails_in_flight: o que aguarda na fila (uma campanha em andamento, envios de automação reservados) — já está prometido. emails_remaining: o que ainda pode partir, fila deduzida, nunca negativo (null para uma oferta ilimitada). resets_at: a redefinição do contador, no dia 1º do mês seguinte às 00:00 UTC (2h em Paris no verão, 1h no inverno).
  • billing — status vale none sem assinatura paga; caso contrário, o do pagamento: trialing (a semana gratuita da primeira migração para uma oferta paga: a oferta se aplica, nada ainda é cobrado; trial_ends_at diz quando cai a primeira cobrança), active, past_due (uma cobrança falhou, a oferta permanece aberta durante as tentativas), unpaid (tentativas abandonadas: o espaço opera nos limites da Descoberta), canceled … subscribed_plan nomeia a oferta faturada — ela pode diferir de plan.id após uma inadimplência. scheduled_change anuncia um downgrade ou cancelamento programado ({ "plan": "free", "effective_at": "…" }). manage_url é a tela onde o proprietário ou um administrador muda de oferta.
  • limits — os membros (um convite pendente ocupa uma vaga), os domínios de envio e daily: o limite em 24 horas corridas de um espaço novo (100 e-mails nos três primeiros dias, 500 até o sétimo), null quando não se aplica.

Um software conectado ao Mailcheer — um CRM que envia por seus usuários, por exemplo — pode assim exibir "restam 470 e-mails até 1º de outubro" em vez de descobrir a recusa. O proprietário e os administradores do espaço, por sua vez, recebem um e-mail a 50%, 80% e 95% da cota, uma vez por mês cada — sem e-mail a 100%: a recusa diz isso.

A cota em cada resposta

Não é preciso chamar GET /api/v1/me antes de cada envio: cada resposta autenticada da API — sucesso ou erro, 402 incluído — e do servidor MCP traz o estado da cota do mês.

CabeçalhoValor
Mailcheer-Quota-LimitE-mails por mês da sua oferta, ou unlimited.
Mailcheer-Quota-UsedPartidos neste mês mais o que aguarda na fila (emails_sent + emails_in_flight).
Mailcheer-Quota-RemainingO que ainda pode partir, fila deduzida, nunca negativo — ou unlimited.
Mailcheer-Quota-ResetA redefinição, em ISO 8601: dia 1º do mês seguinte, 00:00 UTC.
curl -i https://mailcheer.com/api/v1/emails -H "Authorization: Bearer mch_live_…" …
# HTTP/1.1 202 Accepted
# Mailcheer-Quota-Limit: 3000
# Mailcheer-Quota-Used: 2531
# Mailcheer-Quota-Remaining: 469
# Mailcheer-Quota-Reset: 2026-10-01T00:00:00.000Z

A resposta de um envio aceito já conta esse envio. Uma resposta sem chave válida (401) não a traz: ela não conhece o seu espaço. Se a cota não puder ser lida naquele instante, a resposta parte mesmo assim, sem esses cabeçalhos.

Uma chave com seu próprio limite mensal também traz Mailcheer-Key-Quota-Limit, -Used e -Remaining — ver O limite e o idioma de uma chave.

Ser avisado: o webhook quota.threshold_reached

Assine um endereço ao evento quota.threshold_reached (POST /api/v1/webhooks, ou Configurações → API): o Mailcheer o chama quando a cota do mês atinge 50, 80, 95 e 100%, uma vez por mês e por limite, no momento em que o e-mail que cruza o limite é aceito. Se um único envio cruzar vários, apenas o mais alto parte. O corpo é assinado e tem novas tentativas como qualquer evento (ver Os webhooks):

{
  "id": "evt_3kT9xQ2mV7aB1cD4",
  "type": "quota.threshold_reached",
  "created_at": "2026-09-24T16:02:11.000Z",
  "data": {
    "threshold": 80,
    "plan": "free",
    "quota": 3000,
    "sent": 2400,
    "in_flight": 35,
    "remaining": 565,
    "resets_at": "2026-10-01T00:00:00.000Z",
    "period": "2026-09"
  }
}

threshold é o limite atingido, em porcento; os outros campos têm o sentido de details de uma recusa 402. A 100%, apenas o webhook parte (nenhum e-mail): a partir daí, os envios respondem 402 quota_exceeded até resets_at.

Quando a cota não é suficiente

Um envio que não cabe no restante do mês é recusado inteiro, antes de partir: nada é enviado, nada é colocado na fila. A resposta é um 402 de código quota_exceeded, em POST /api/v1/emails como em POST /api/v1/campaigns/CAMP_ID/send, e a ferramenta MCP que faz o mesmo gesto retorna o mesmo erro:

{
  "error": {
    "code": "quota_exceeded",
    "message": "…",
    "details": {
      "plan": "free",
      "quota": 3000,
      "sent": 2940,
      "in_flight": 20,
      "remaining": 40,
      "requested": 250,
      "resets_at": "2026-10-01T00:00:00.000Z"
    }
  },
  "statusCode": 402,
  "message": "…",
  "name": "quota_exceeded"
}

remaining vale quota − sent − in_flight; requested é o que a chamada pedia (destinatários, cópias incluídas). Dois caminhos: mudar de oferta (billing.manage_url), ou aguardar resets_at. Tentar a mesma chamada antes de um dos dois retornará a mesma recusa.

A oferta gratuita: um domínio, um espaço por mês

Na oferta gratuita, os 3.000 e-mails do mês estão vinculados ao domínio de envio, não à conta. Um domínio registrado (acme.com, subdomínios incluídos) só serve à oferta gratuita em um único espaço por mês UTC: o primeiro que envia com ele. Outro espaço gratuito que envia desse domínio, ou de um de seus subdomínios, no mesmo mês recebe outro 402, recusado inteiro também:

{
  "error": {
    "code": "free_plan_domain_used",
    "message": "L'offre gratuite vaut pour un domaine d'envoi, pas pour un compte : ce domaine (news.acme.com, de acme.com) l'a déjà utilisée ce mois-ci dans un autre espace. Passez à une offre payante pour envoyer depuis plusieurs espaces, ou attendez le 1er octobre à 0 h UTC (2 h à Paris).",
    "details": {
      "domain": "news.acme.com",
      "root_domain": "acme.com",
      "period": "2026-09",
      "resets_at": "2026-10-01T00:00:00.000Z"
    }
  },
  "statusCode": 402,
  "message": "…",
  "name": "free_plan_domain_used"
}

Distinga os dois 402 por error.code. Passar para uma oferta paga remove este imediatamente; as ofertas pagas não são afetadas.

Nunca enviar duas vezes

Uma biblioteca HTTP que não recebeu nossa resposta repete a chamada. É o trabalho dela, e sem precaução seu cliente recebe duas vezes a mesma fatura.

Adicione o cabeçalho Idempotency-Key com um valor único por envio — o número da fatura, o identificador do pedido, um UUID:

Idempotency-Key: facture-2026-0412

Repetir uma chamada bem-sucedida retorna a mesma resposta, com o mesmo id, sem segundo envio. O cabeçalho Idempotent-Replay: true informa que é uma repetição. Uma chave é mantida por 24 horas após a primeira chamada; além disso, a mesma chamada é um novo envio.

Uma recusa não é mantida. Um 402 (cota), um 423 (revisão de segurança), um 429 (limite diário) ou um 422 (campo inválido) não enviaram nada: uma vez resolvida a causa, reenvie a mesma chamada com a mesma chave, ela parte. É isso que sua lógica de nova tentativa deve fazer, sem nova chave.

Duas chamadas idênticas no mesmo instante enviam apenas uma vez: a segunda aguarda a primeira e retorna sua resposta. Se a primeira ainda estiver em andamento após alguns segundos, a segunda recebe 409 idempotency_key_in_use com Retry-After: reenvie-a como está. A mesma chave com um corpo diferente responde 409 idempotency_key_reused: não é uma nova tentativa, é um erro do seu lado, e devolver a resposta de outro envio seria pior do que informar.

Os assinantes

# Ajouter — la personne reçoit une confirmation et entre en « pending »
curl -X POST https://mailcheer.com/api/v1/subscribers \
  -H "Authorization: Bearer mch_live_…" \
  -H "Content-Type: application/json" \
  -d '{"email":"marie@exemple.fr","firstName":"Marie","tags":["clients"]}'

# Lister, page par page
curl "https://mailcheer.com/api/v1/subscribers?limit=50&status=subscribed" \
  -H "Authorization: Bearer mch_live_…"

# Désinscrire
curl -X DELETE https://mailcheer.com/api/v1/subscribers/marie%40exemple.fr \
  -H "Authorization: Bearer mch_live_…"

DELETE não apaga a ficha: a pessoa passa para unsubscribed e seu endereço entra na lista de supressão. Apagar o rastro a faria voltar na próxima importação de arquivo — teríamos respeitado o verbo HTTP e traído a pessoa.

Desinscrita, a pessoa não recebe mais suas campanhas, suas automações nem seus envios de API que carregam unsubscribe_url. Seus e-mails individuais — uma resposta, uma fatura — ainda chegam até ela.

Um endereço desinscrito não se reinscreve pela API. Somente a pessoa pode voltar, por um formulário. Uma desinscrição que um programa pode cancelar não vale nada.

A paginação

As listas retornam { data, has_more, next_cursor }. Passe next_cursor em ?cursor= para a próxima página.

Sem número de página, propositalmente: em uma lista onde se escreve ao mesmo tempo em que se lê — que é exatamente o caso de uma API — page=2 pula linhas e mostra outras duas vezes. O cursor, por sua vez, não se move.

Somente o que mudou: updated_since

Para manter uma cópia da sua lista atualizada sem reler tudo, passe updated_since (ISO 8601): você recebe apenas as fichas criadas ou modificadas desde então — status (confirmação, desinscrição, rebote), nome, campos personalizados, etiquetas.

curl "https://mailcheer.com/api/v1/subscribers?updated_since=2026-09-25T08:00:00Z" \
  -H "Authorization: Bearer mch_live_…"

Cada ficha carrega updated_at. Guarde a maior recebida e, na próxima vez, passe-a menos um minuto: ela vem do nosso relógio, não do seu, e o minuto dá uma segunda chance a uma modificação em andamento no momento exato da sua leitura. Uma ficha vista duas vezes é simplesmente uma atualização.

As etiquetas

POST /api/v1/subscribers também coloca etiquetas, mas isso é uma inscrição: em uma pessoa ainda pending, ele reenvia o e-mail de confirmação. Para alterar etiquetas, e somente elas, use estas chamadas — sem inscrição, sem e-mail, sem tocar no status:

# Un abonné : poser et retirer en un appel
curl -X POST https://mailcheer.com/api/v1/subscribers/marie%40example.com/tags \
  -H "Authorization: Bearer mch_live_…" \
  -H "Content-Type: application/json" \
  -d '{ "add": ["client"], "remove": ["prospect"] }'

# Une étiquette, plusieurs abonnés : jusqu'à 500 adresses dans chaque liste
curl -X POST https://mailcheer.com/api/v1/tags/client/subscribers \
  -H "Authorization: Bearer mch_live_…" \
  -H "Content-Type: application/json" \
  -d '{ "add": ["marie@example.com", "paul@example.com"], "remove": ["ancien@example.com"] }'

A primeira responde com a ficha e o que realmente mudou (added, removed): colocar uma etiqueta já existente, ou remover uma etiqueta ausente, não é um erro — é aí que um erro de digitação aparece. A ficha deve existir: um endereço desconhecido responde 404 e nada é criado. A segunda responde com contagens (added, removed, unchanged) e os endereços que não são assinantes do espaço (not_found), também nunca criados. Uma etiqueta nova é criada quando add a nomeia.

Uma etiqueta é designada pelo seu id ou pelo seu nome, codificado no endereço (/api/v1/tags/clients%20VIP).

ChamadaO que ela faz
GET /api/v1/tagsTodas as etiquetas, com subscribers (quantas pessoas a carregam) e subscribed (quantos deles estão ativos).
PATCH /api/v1/tags/{tag}Renomeia: { "name": "…" }. Os assinantes a mantêm sob o novo nome; um nome já existente responde 409.
DELETE /api/v1/tags/{tag}Exclui. Os assinantes permanecem, apenas a etiqueta é removida deles. Recusada em 409 enquanto uma automação, um segmento ou um formulário de inscrição a utiliza — details.used_by os nomeia: sem ela, eles continuariam silenciosamente, sem alcançar mais ninguém.

Adicionar em lote

POST /api/v1/subscribers/batch adiciona ou atualiza até 500 assinantes em uma chamada. Cada entrada passa exatamente pelo caminho de POST /api/v1/subscribers — double opt-in por padrão, manutenção das confirmações, lista de supressão, desinscritos que não fazemos voltar.

curl -X POST https://mailcheer.com/api/v1/subscribers/batch \
  -H "Authorization: Bearer mch_live_…" \
  -H "Content-Type: application/json" \
  -d '{ "subscribers": [
        { "email": "marie@example.com", "firstName": "Marie", "tags": ["clients"] },
        { "email": "paul@example.com", "tags": ["clients"] }
      ] }'

Uma linha recusada não faz as outras falharem. A resposta retorna uma linha por entrada, na ordem: created ou updated com os campos da adição unitária (subscriber, confirmation_sent …), ou rejected com o error que a adição unitária teria retornado — código, mensagem, detalhes. Um summary conta os três. O mesmo endereço duas vezes em um lote: a segunda linha é recusada.

Um lote de N conta como N requisições no limite de taxa: ele economiza idas e voltas, mas não aumenta o limite. Um lote que não cabe no minuto é recusado inteiro, sem consumir nada (429, Retry-After).

Apagar uma pessoa (LGPD)

Quando uma pessoa solicita a exclusão dos seus dados — não apenas deixar de receber e-mails —, é:

curl -X POST https://mailcheer.com/api/v1/subscribers/marie%40example.com/erase \
  -H "Authorization: Bearer mch_live_…"
# → { "object": "subscriber", "email": "marie@example.com", "erased": true, "suppressed": false }

Irreversível. A ficha, o nome, os campos personalizados, a prova de consentimento e as etiquetas são excluídos. O que foi prometido à pessoa e não foi enviado não será enviado; seus fluxos de automação são interrompidos. As estatísticas das campanhas já enviadas não mudam: cada mensagem permanece contada, sem nada mais que a relacione à pessoa.

É uma rota separada, não um parâmetro de DELETE: este DELETE desinscreve e mantém a ficha, e um parâmetro mal escrito faria silenciosamente o outro gesto, irreversível. A lista de supressão não é tocada — um endereço que está nela permanece (suppressed: true), é isso que garante que nada mais será enviado a ela. Remover um endereço dessa lista é feito manualmente, no seu espaço.

As campanhas

Criar e enviar são dois gestos separados. Não é burocracia: é o que permite revisar uma carta antes de ela partir para três mil pessoas.

# 1. Le brouillon — rien ne part
curl -X POST https://mailcheer.com/api/v1/campaigns \
  -H "Authorization: Bearer mch_live_…" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Lettre de septembre",
    "subject": "Ce que nous avons appris cet été",
    "text": "# Bonjour\n\nVoici les nouvelles du mois."
  }'

# 2. L'envoi — irréversible
curl -X POST https://mailcheer.com/api/v1/campaigns/CAMP_ID/send \
  -H "Authorization: Bearer mch_live_…"

# 3. Le suivi
curl https://mailcheer.com/api/v1/campaigns/CAMP_ID \
  -H "Authorization: Bearer mch_live_…"

Em text, uma linha vazia separa dois parágrafos e # no início da linha faz um título. Para a diagramação completa (imagens, botões, separadores), passe content com os blocos do editor.

O envio responde 202 com queued: os destinatários são fixados, as mensagens partem depois na taxa permitida pelo nosso provedor. Um envio de cinquenta mil e-mails não cabe em uma requisição HTTP, e afirmar o contrário lhe daria um "enviado" para um trabalho que mal começou.

As taxas de abertura e clique são calculadas sobre as mensagens entregues, nunca sobre o número de destinatários: um endereço morto não deve diminuir a taxa daqueles que realmente receberam.

As aberturas só são contadas nas mensagens que carregavam o pixel de medição. Quando a configuração Medir aberturas do espaço estava desligada para todo o envio, opens_tracked vale false, e open_rate e human_open_rate valem null — nunca um 0% enganoso.

Cada taxa existe duas vezes: open_rate e click_rate contam os robôs, como a maioria das ferramentas; human_open_rate e human_click_rate os excluem. Um robô é uma abertura ou um clique nos dois minutos seguintes à entrega (os gateways de segurança das mensagerias corporativas visitam cada link na chegada da mensagem, os relays de privacidade pré-carregam as imagens), ou vindo de um robô que se declara no seu agente, ou de uma faixa de endereços que seu operador publica (Google, Bing). Os relays de privacidade (Apple Mail, Gmail, Yahoo) não são robôs em si. A regra é propositalmente estrita: uma pessoa que abre no primeiro minuto é contada como robô — uma taxa um pouco baixa em vez de uma taxa inflada.

Excluir um rascunho

curl -X DELETE https://mailcheer.com/api/v1/campaigns/CAMP_ID \
  -H "Authorization: Bearer mch_live_…"

Responde { "object": "campaign", "id": "…", "deleted": true }. Somente um rascunho (draft) pode ser excluído, e é definitivo. Uma campanha agendada, em andamento, enviada ou arquivada responde 409 conflict, com seu status em details.status: o que já foi enviado, ou será enviado, mantém seus envios e suas estatísticas.

Modificar um rascunho

curl -X PATCH https://mailcheer.com/api/v1/campaigns/CAMP_ID \
  -H "Authorization: Bearer mch_live_…" \
  -H "Content-Type: application/json" \
  -d '{ "subject": "Ce que nous avons appris cet été (bis)", "text": "# Bonjour\n\nLa nouvelle version." }'

Os campos da criação, todos opcionais: name, subject, preheader, kind, from ou sender_id, e o conteúdo — text ou content, não ambos. Um novo conteúdo substitui o antigo; sem preheader, ele mantém o do rascunho. A resposta é a campanha, como GET a retorna. Nada é enviado.

Somente um rascunho pode ser modificado: qualquer outro status responde 409 conflict com details.status, e nada é alterado. Um campo desconhecido é recusado (422), ao contrário da criação: um subjet mal escrito, ignorado silenciosamente, deixaria o antigo assunto partir para toda a lista.

Escrever para um grupo em vez de para a lista inteira

Sem corpo, o envio parte para todo o alvo da campanha: todos os assinantes ativos do espaço (ou do segmento escolhido na interface), menos a lista de supressão. Para escrever apenas para uma parte — aqueles que não responderam, seus clientes, os inscritos de um workshop — passe seus endereços em to:

curl -X POST https://mailcheer.com/api/v1/campaigns/CAMP_ID/send \
  -H "Authorization: Bearer mch_live_…" \
  -H "Content-Type: application/json" \
  -d '{ "to": ["claire@exemple.fr", "marc@exemple.fr", "ancien@exemple.fr"] }'

to só pode restringir. A carta parte para os endereços solicitados que também são assinantes ativos do alvo, e nunca para um endereço da lista de supressão: uma pessoa desinscrita, cujo endereço rebotou ou que reclamou não recebe nada, mesmo que seu endereço esteja em to. A resposta diz quem foi retido, quem foi excluído e por quê:

{
  "id": "cmp_8d2", "object": "campaign", "status": "sending", "queued": 2,
  "audience": {
    "requested": 3, "duplicates": 0, "retained": 2, "excluded": 1,
    "reasons": { "invalid": 0, "not_in_list": 0, "pending": 0, "unsubscribed": 1,
                 "bounced": 0, "complained": 0, "suppressed": 0, "outside_segment": 0 },
    "excluded_addresses": [ { "email": "ancien@exemple.fr", "reason": "unsubscribed" } ]
  },
  "note": "2 destinataire(s) mis en file. …"
}

A contagem sempre fecha: requested = duplicates + retained + excluded. Maiúsculas e espaços são ignorados (Claire@Exemple.fr é a mesma pessoa). As razões: invalid (não é um endereço), not_in_list (nenhum assinante do espaço o carrega), pending (inscrição ainda não confirmada), unsubscribed, bounced, complained, suppressed (assinante, mas na lista de supressão) e outside_segment (fora do segmento que a campanha visa).

Três regras a conhecer:

  • Uma lista vazia não é "sem lista". "to": [] não escreve para ninguém: o envio é recusado (422). Para escrever para a lista inteira, não envie o campo to de forma alguma.
  • Sem to em uma campanha com teste A/B. A versão vencedora parte horas depois, calculada sobre todo o alvo da campanha; a lista de endereços não sobreviveria a isso. O envio é recusado em vez de partir para todos.
  • Campos desconhecidos são ignorados, exceto sósias de dry_run e de to. dryRun, dry-run, DRY_RUN, test, simulate, preview … e To, TO, recipients, emails, to_emails, destinataires, adresses, audience … são recusados (422) nomeando o campo correto: ignorados, os primeiros fariam a campanha partir de verdade, os segundos a fariam partir para a lista inteira. POST /api/v1/audience recusa qualquer campo desconhecido.

Até 50.000 endereços por chamada.

Simular antes de enviar

"dry_run": true faz todas as verificações de um envio real — remetente, conteúdo, reputação, cota, destinatários — e não coloca nada na fila. A resposta é um 200:

{ "id": "cmp_8d2", "object": "send_preview", "dry_run": true,
  "would_send": true, "blocked_reason": null, "recipients": 2,
  "audience": { "requested": 3, "retained": 2, "excluded": 1, … } }

Se o envio real seria recusado, would_send vale false e blocked_reason dá a mensagem exata que ele retornaria. É o número a mostrar à pessoa antes de ela confirmar.

Para fazer a mesma pergunta antes de criar a campanha — enquanto se escolhe para quem escrever, no seu próprio software — POST /api/v1/audience retorna o mesmo balanço sem criar nada (direito subscribers:read):

curl -X POST https://mailcheer.com/api/v1/audience \
  -H "Authorization: Bearer mch_live_…" \
  -H "Content-Type: application/json" \
  -d '{ "to": ["claire@exemple.fr", "marc@exemple.fr"] }'
# → { "object": "audience", "recipients": 2, "audience": { … } }

Sem to, ele retorna o número de assinantes que uma campanha enviada para a lista inteira alcançaria. As verificações específicas de uma campanha (assunto, conteúdo, cota) permanecem as de dry_run.

O relatório completo

GET /api/v1/campaigns/CAMP_ID/stats retorna tudo o que a ficha de campanha do Mailcheer mostra, para exibi-lo no seu próprio software — um CRM, um painel:

{
  "campaign": { "id": "…", "name": "…", "subject": "…", "status": "sent", "kind": "newsletter",
                "fromName": "…", "fromEmail": "…", "sentAt": "…", "scheduledAt": null, "updatedAt": "…" },
  "counts": { "recipients": 66, "delivered": 60, "opened": 30, "clicked": 10,
              "humanOpened": 22, "humanClicked": 7,
              "bounced": 6, "complained": 1, "unsubscribed": 0 },
  "rates": { "delivered": 0.9091, "opened": 0.5, "clicked": 0.1667,
             "humanOpened": 0.3667, "humanClicked": 0.1167, "bounced": 0.0909 },
  "bots": { "opened": 9, "clicked": 4, "delay": 12, "scanner": 1 },
  "timeline": [ { "label": "+0h", "opened": 12, "clicked": 5, "humanOpened": 4, "humanClicked": 1 }, … ],
  "audience": { "total": 30, "proxiedShare": 0.4,
                "device": [ { "label": "Téléphone", "count": 15, "share": 0.5 }, … ],
                "os": [ … ], "client": [ … ] },
  "links": [ { "url": "https://…", "clicks": 6 }, … ],
  "html": "<!doctype html>…"
}

As partes (rates, share, proxiedShare) estão entre 0 e 1, e null enquanto não há nada para dividir. opened e clicked contam os robôs, humanOpened e humanClicked os descartam; untracked conta as mensagens entregues que saíram sem pixel de medição de aberturas — as aberturas e suas taxas incidem apenas sobre as demais, e rates.opened vale null se não restar nenhuma; bots informa quantas aberturas e cliques foram descartados, em eventos, e por quê (delay: nos dois minutos seguintes à entrega; scanner: um robô que se declara ou um endereço publicado por seu operador). timeline conta as aberturas e os cliques por faixa de seis horas durante as 48 primeiras horas após o envio, com e sem robôs — vazio enquanto a campanha não tiver partido. audience conta apenas as aberturas de pessoas e mantém apenas as cinco primeiras linhas de cada distribuição; proxiedShare é a parcela dessas aberturas vindas de um relé de privacidade (Apple Mail, Gmail): recebidas, não necessariamente lidas. links vai do mais ao menos clicado, por pessoas. html é a mensagem tal como partiu, string vazia caso contrário.

Quem abriu: os destinatários

GET /api/v1/campaigns/CAMP_ID/recipients retorna uma linha por pessoa para quem a campanha partiu, paginada por cursor como /subscribers (?limit= até 100, ?cursor= = o next_cursor da página anterior):

curl "https://mailcheer.com/api/v1/campaigns/CAMP_ID/recipients?status=opened" \
  -H "Authorization: Bearer mch_live_…"
{
  "object": "list",
  "data": [
    { "object": "recipient", "id": "…", "email": "claire@exemple.fr",
      "first_name": "Claire", "last_name": "Martin", "status": "clicked",
      "sent_at": "…", "delivered_at": "…", "opened_at": "…", "clicked_at": "…",
      "human_opened": true, "human_clicked": true, "unsubscribed": false },
    { "object": "recipient", "id": "…", "email": null, … }
  ],
  "has_more": true,
  "next_cursor": "…"
}

?status= filtra por opened, clicked, not_opened (partiu, sem rebote, nunca aberto), bounced, complained ou unsubscribed. opened_at e clicked_at contam os robôs, como opened e clicked do relatório; human_opened e human_clicked indicam se uma pessoa abriu ou clicou. email vale null quando o contato foi excluído após o envio: a linha permanece, ela conta nos números da campanha. unsubscribed vale true quando a pessoa se descadastrou pelo link desta mensagem: o link de descadastro designa a carta que o carrega, e um descadastro feito em outro lugar (pela API, no aplicativo, a partir de uma automação) não conta em nenhuma campanha. Para uma carta partida antes de 24/09/2026, cujo link designava apenas a pessoa, o descadastro permanece atribuído à última carta recebida antes dela. O relatório (/stats) conta os mesmos em counts.unsubscribed.

Os webhooks

A API responde quando é consultada; um webhook avisa sem que se peça. Assine um endereço seu — POST /api/v1/webhooks com uma chave que tenha a permissão webhooks:write, ou Configurações → API —, escolha seus eventos, e o Mailcheer envia um POST com um corpo JSON cada vez que um deles ocorre.

curl -X POST https://mailcheer.com/api/v1/webhooks \
  -H "Authorization: Bearer mch_live_…" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Mon CRM",
    "url": "https://votre-service.fr/mailcheer/evenements",
    "events": [
      "email.bounced",
      "email.complained",
      "subscriber.unsubscribed"
    ]
  }'

A resposta traz o secret de assinatura da assinatura (whsec_…). Guarde-o: é ele que permite ao seu serviço saber que uma chamada vem realmente do Mailcheer. O endereço deve estar em https, e nunca um endereço de rede privada.

Todos os eventos

EventoEnviado quando
email.sentUm e-mail enviado pela API (POST /api/v1/emails, a ferramenta MCP send_email) foi aceito pelo nosso provedor de envio — um evento por endereço, to, cc e bcc. Apenas e-mails de API.
email.failedUm e-mail enviado pela API foi registrado mas não partiu; reason informa o motivo. Uma recusa que não registrou nada (cota, domínio, lista de supressão…) não produz nenhum: não há e-mail, apenas uma resposta de erro.
email.deliveredO servidor de e-mail do destinatário aceitou o e-mail.
email.openedO e-mail foi aberto — o pixel de medição foi carregado, robôs incluídos, como opened_at.
email.clickedUm link do e-mail foi clicado; url informa qual.
email.bouncedO e-mail voltou; reason informa o tipo. Um rebote Permanent coloca o endereço na lista de supressão.
email.complainedO destinatário marcou o e-mail como indesejado. O endereço entra na lista de supressão.
email.receivedUm destinatário respondeu, em reply.<domaine> de um domínio onde o recebimento de respostas está ativado — veja abaixo.
subscriber.createdUm assinante foi adicionado: formulário de inscrição, API ou MCP, ou manualmente no aplicativo.
subscriber.unsubscribedUm assinante saiu: link de descadastro de um e-mail, API ou MCP, ou aplicativo.
quota.threshold_reachedA cota do mês atingiu 50, 80, 95 ou 100% — veja Ser avisado.

Todos os corpos têm o mesmo envelope: id (evt_…, o mesmo a cada tentativa e a cada reexecução do evento), type, created_at (o momento do evento, em ISO 8601 UTC — não o da entrega) e data, cujos campos dependem do evento. Um evento produzido por uma chave de teste carrega adicionalmente "test": true, entre created_at e data — veja Os eventos do modo de teste.

Os campos dos eventos email.*

CampoPresenteSignificado
message_idsempreCom source: "api", o id retornado por POST /api/v1/emails. Com source: "campaign", o id da linha do destinatário em GET /api/v1/campaigns/{id}/recipients.
emailsempreO endereço envolvido no evento. Para um e-mail de campanha cujo contato foi excluído após o envio, uma string vazia.
sourcesemprecampaign ou api.
campaign_id, campaign_namee-mails de campanhaA campanha que enviou o e-mail.
subscriber_ide-mails de campanha cujo contato ainda existeO assinante que o recebeu.
subjectquando o e-mail tem umO assunto da campanha ou do e-mail.
urlemail.clickedO link clicado.
reasonemail.bounced, email.failedPara um rebote, o tipo relatado pelo Amazon SES: Permanent (o endereço não existe), Transient (caixa cheia, servidor indisponível) ou Undetermined. Para uma falha: provider_rejected (nosso provedor de envio recusou a mensagem), safety_review (encaminhado para uma revisão de segurança humana — a chamada respondeu 423: reenvie após a decisão), suspended (o espaço foi suspenso pela revisão) ou interrupted (uma falha do nosso lado antes do envio).
recipient_typee-mails de API (source: "api")to se o endereço era um destinatário, cc ou bcc se era uma cópia.
{
  "id": "evt_Ls2kPq8Rw4Tn6Vx1",
  "type": "email.sent",
  "created_at": "2026-09-25T08:31:05.118Z",
  "data": {
    "message_id": "cmu651xf200021n7nm68sikfw",
    "email": "client@example.com",
    "source": "api",
    "subject": "Votre facture de septembre",
    "recipient_type": "to"
  }
}
{
  "id": "evt_Fm9sQw3Er7Ty1Ui4",
  "type": "email.failed",
  "created_at": "2026-09-25T08:40:12.604Z",
  "data": {
    "message_id": "cmu652bq900031n7n0x9h2rst",
    "email": "client@example.com",
    "source": "api",
    "subject": "Votre facture de septembre",
    "reason": "provider_rejected",
    "recipient_type": "to"
  }
}
{
  "id": "evt_8fQm2LxT0aVb7KcN",
  "type": "email.delivered",
  "created_at": "2026-09-25T08:31:07.412Z",
  "data": {
    "message_id": "cmu651xf200021n7nm68sikfw",
    "email": "client@exemple.fr",
    "source": "api",
    "subject": "Votre facture de septembre"
  }
}
{
  "id": "evt_Jr4uWc9ZpQe1sHa2",
  "type": "email.opened",
  "created_at": "2026-09-25T09:02:44.108Z",
  "data": {
    "message_id": "cmu7a2k9q000b1n7n3v5c8xyz",
    "email": "claire@exemple.fr",
    "source": "campaign",
    "campaign_id": "cmu79z1ab00001n7nqk2d4efg",
    "campaign_name": "Lettre d'octobre",
    "subscriber_id": "cmu5x0p3r00041n7n8m2t6hij",
    "subject": "Les nouveautés du mois"
  }
}
{
  "id": "evt_Tn7bKx2VdMf0gLq5",
  "type": "email.clicked",
  "created_at": "2026-09-25T09:03:12.550Z",
  "data": {
    "message_id": "cmu7a2k9q000b1n7n3v5c8xyz",
    "email": "claire@exemple.fr",
    "source": "campaign",
    "campaign_id": "cmu79z1ab00001n7nqk2d4efg",
    "campaign_name": "Lettre d'octobre",
    "subscriber_id": "cmu5x0p3r00041n7n8m2t6hij",
    "subject": "Les nouveautés du mois",
    "url": "https://votredomaine.fr/offre"
  }
}
{
  "id": "evt_Ab3cD9eF0gH1iJ2k",
  "type": "email.bounced",
  "created_at": "2026-09-25T08:31:09.020Z",
  "data": {
    "message_id": "cmu651xf200021n7nm68sikfw",
    "email": "personne@exemple.fr",
    "source": "api",
    "subject": "Votre facture de septembre",
    "reason": "Permanent"
  }
}
{
  "id": "evt_Pw6yRz1Xc0Vb8Nm3",
  "type": "email.complained",
  "created_at": "2026-09-25T10:15:31.774Z",
  "data": {
    "message_id": "cmu7a2k9q000c1n7n0w4d7abc",
    "email": "marc@exemple.fr",
    "source": "campaign",
    "campaign_id": "cmu79z1ab00001n7nqk2d4efg",
    "campaign_name": "Lettre d'octobre",
    "subscriber_id": "cmu5x0p3r00051n7n2k9s1klm",
    "subject": "Les nouveautés du mois"
  }
}

Os campos de email.received

Uma resposta de um destinatário, recebida em reply.<votre domaine>. O evento só existe para um domínio onde o recebimento de respostas está ativado: no aplicativo, tela do domínio → Receber respostas, uma linha de DNS a configurar.

TipoNomeValorPrioridade
MXreply.votredomaine.frinbound-smtp.eu-west-1.amazonaws.com10

Esta linha não interfere na sua caixa de entrada: ela vale apenas para o subdomínio reply. Uma vez lida, cada e-mail de API desse domínio enviado sem reply_to parte com o endereço de resposta <partie locale>+<id>@reply.<domaine> — o id é o que retorna POST /api/v1/emails. Um reply_to fornecido na chamada é sempre respeitado, e qualquer endereço em reply.<domaine> que você escolher (dossier-4821@reply.votredomaine.fr) também é recebido. Enquanto a linha não for lida, nada muda: as respostas vão para o endereço de envio. Desativar o recebimento interrompe o endereço de resposta automático dos novos e-mails; as respostas aos e-mails já enviados continuam chegando enquanto a linha existir.

CampoPresenteSignificado
reply_idsempreO identificador da resposta no Mailcheer.
emailsempreO endereço da pessoa que respondeu — o mesmo que from, para que data.email designe o contato em todos os email.*.
fromsempreO remetente da resposta.
from_namequando tem um nomeSeu nome exibido.
tosempreOs endereços em reply.<domaine> que receberam a resposta.
ccquando houverOs endereços em cópia oculta.
subjectsempreO assunto, decodificado.
textquando a resposta tem uma versão em textoO texto, como está, incluindo a citação do seu e-mail.
htmlquando a resposta tem uma versão em HTMLO HTML, como está. Ele vem de um desconhecido: nunca o exiba sem limpá-lo.
received_atsempreA hora de recebimento pela Amazon, em ISO 8601 UTC.
headerssempremessage_id, in_reply_to, references (array), date e auto_submitted — null quando faltam.
attachmentssempre, às vezes vazioPara cada anexo: filename, content_type, size (em bytes), content_id (imagem incorporada), url — um link de download assinado, que não exige nenhuma chave — e url_expires_at: o link vale 7 dias.
message_idquando o envio original é encontradoO id do e-mail do Mailcheer ao qual a pessoa responde, como nos outros email.*. Encontrado pelo endereço de resposta, ou por In-Reply-To e References — nunca adivinhado.
sourcecom message_idapi, campaign ou automation.
campaign_idresposta a um e-mail de campanhaA campanha.
automation_idresposta a um e-mail de automaçãoA automação.
tagsresposta a um e-mail de API que o carregavaOs tags do envio original, para organizar a resposta no seu lado.
spamsempretrue quando o filtro da Amazon julga a resposta indesejada.
automaticsempretrue para uma resposta automática: ausência, aviso de recebimento, relatório de entrega (Auto-Submitted, Precedence …).
authenticationsemprespf, dkim e dmarc: o veredito da Amazon sobre o remetente (PASS, FAIL, GRAY …).
{
  "id": "evt_R3pL9yQ2wE5tY8uI",
  "type": "email.received",
  "created_at": "2026-09-25T15:02:11.520Z",
  "data": {
    "reply_id": "cmu8r2d5k00031n7nq4w9pxyz",
    "email": "claire@exemple.fr",
    "from": "claire@exemple.fr",
    "from_name": "Claire Martin",
    "to": [
      "facturation+cmu651xf200021n7nm68sikfw@reply.votredomaine.fr"
    ],
    "subject": "Re: Votre facture de septembre",
    "text": "Bonjour, pouvez-vous l'envoyer aussi à notre comptable ?\n\nLe 25 sept. 2026, Votre entreprise a écrit :\n> Vous trouverez votre facture en pièce jointe.",
    "received_at": "2026-09-25T15:02:11.482Z",
    "headers": {
      "message_id": "CAF3k9x@mail.gmail.com",
      "in_reply_to": "0102019a7c3e1b2f-3d1e-000000@eu-west-1.amazonses.com",
      "references": [
        "0102019a7c3e1b2f-3d1e-000000@eu-west-1.amazonses.com"
      ],
      "date": "Thu, 25 Sep 2026 17:02:08 +0200",
      "auto_submitted": null
    },
    "attachments": [
      {
        "filename": "bon-de-commande.pdf",
        "content_type": "application/pdf",
        "size": 48213,
        "url": "https://mailcheer.com/api/reponses/pj/Y211OHIy…~kQ3v…",
        "url_expires_at": "2026-10-02T15:02:11.520Z"
      }
    ],
    "message_id": "cmu651xf200021n7nm68sikfw",
    "source": "api",
    "tags": {
      "client": "4821"
    },
    "spam": false,
    "automatic": false,
    "authentication": {
      "spf": "PASS",
      "dkim": "PASS",
      "dmarc": "PASS"
    }
  }
}

Três limites a conhecer:

  • No máximo 150 Ko por resposta, cabeçalhos e anexos codificados incluídos. Acima disso, a Amazon a recusa, e seu remetente recebe uma mensagem de erro: ela nunca chega.
  • Uma mensagem que o antivírus da Amazon condena não é guardada nem anunciada.
  • As respostas permanecem 30 dias na tela Respostas do aplicativo; depois, apenas o seu software guarda a cópia.

Os campos dos eventos subscriber.*

CampoPresenteSignificado
subscriber_idsempreO identificador do assinante.
emailsempreO endereço dele.
statussemprepending (aguardando confirmação), subscribed ou unsubscribed.
first_name, last_namena criação, quando conhecidosO primeiro nome e o sobrenome.
sourcena criaçãoform — um formulário de inscrição, status: "pending" até a pessoa confirmar. api — a API ou o MCP: pending, ou subscribed com double_opt_in: false. manual — adicionado no aplicativo: subscribed, ou unsubscribed se o endereço estiver na lista de supressão.
reasonno cancelamento da inscriçãounsubscribe (o link de um e-mail, DELETE /api/v1/subscribers/{email}, o aplicativo), ou o motivo informado na entrada na lista de supressão: manual, unsubscribe, bounce ou complaint.
campaign_id, message_idcancelamento pelo link de um e-mail de campanhaA campanha e a linha do destinatário (message_id) cujo link foi usado.
automation_idcancelamento pelo link de um e-mail de automaçãoA automação que o enviou.
{
  "id": "evt_Hs5tUv6Wx7Yz8Ab9",
  "type": "subscriber.created",
  "created_at": "2026-09-25T11:20:05.301Z",
  "data": {
    "subscriber_id": "cmu7c4n2p00071n7n5r8t2def",
    "email": "lea@exemple.fr",
    "status": "pending",
    "first_name": "Léa",
    "last_name": "Morel",
    "source": "form"
  }
}
{
  "id": "evt_Kq1wE2rT3yU4iO5p",
  "type": "subscriber.unsubscribed",
  "created_at": "2026-09-25T12:41:58.866Z",
  "data": {
    "subscriber_id": "cmu5x0p3r00041n7n8m2t6hij",
    "email": "claire@exemple.fr",
    "status": "unsubscribed",
    "reason": "unsubscribe",
    "campaign_id": "cmu79z1ab00001n7nqk2d4efg",
    "message_id": "cmu7a2k9q000b1n7n3v5c8xyz"
  }
}

Três coisas que esses eventos não fazem:

  • A confirmação de um assinante pending não gera evento. Leia GET /api/v1/subscribers/{email}: o status dele passa para subscribed.
  • Um bounce ou uma reclamação não gera subscriber.unsubscribed. Ele chega em email.bounced ou email.complained.
  • A mesma pessoa pode ser anunciada como cancelada mais de uma vez — um segundo clique no link, por exemplo. Trate subscriber.unsubscribed como idempotente.

O que cada chamada carrega

Um POST com Content-Type: application/json, User-Agent: Mailcheer-Webhooks/1.0 (+https://mailcheer.com/docs/api) e estes cabeçalhos:

CabeçalhoValor
Mailcheer-Signaturet=<horodatage unix>,v1=<hex> — veja abaixo.
Mailcheer-TimestampO mesmo t, em segundos.
Mailcheer-Event-IdO id do evento: o mesmo a cada tentativa e a cada replay.
Mailcheer-Event-TypeO type do evento.
Mailcheer-Delivery-IdEsta entrega. Um replay manual recebe uma nova.
Mailcheer-AttemptO número da tentativa, a partir de 1.

Verificar a assinatura

v1 é o HMAC-SHA256, em hexadecimal, de "<t>.<corps brut>", com o segredo da assinatura como chave. Três regras:

  1. Calcule-o sobre o corpo bruto, tal como chegou. Um corpo lido e reescrito muda por um espaço ou por uma ordem de chaves, e a assinatura junto.
  2. Compare em tempo constante.
  3. Recuse um t com mais de 300 segundos de diferença do seu relógio. A assinatura continua válida para sempre, o carimbo de data/hora não: é isso que impede de reproduzir mais tarde uma chamada interceptada.

A função com a qual a Mailcheer verifica suas próprias assinaturas, pronta para copiar (Node.js):

import { createHmac, timingSafeEqual } from "node:crypto";

// secret : "whsec_…"
// entete : l'en-tête Mailcheer-Signature
// corpsBrut : le corps tel qu'il est arrivé, intact
export function verifierSignature(
  secret,
  entete,
  corpsBrut,
  maintenantS = Math.floor(Date.now() / 1000),
) {
  if (!entete) return false;
  const parts = new Map();
  for (const morceau of entete.split(",")) {
    const i = morceau.indexOf("=");
    if (i > 0) {
      parts.set(morceau.slice(0, i).trim(), morceau.slice(i + 1));
    }
  }
  const t = Number(parts.get("t"));
  const v1 = parts.get("v1");
  if (!Number.isFinite(t) || !v1) return false;
  if (Math.abs(maintenantS - t) > 300) return false;
  const attendu = Buffer.from(
    createHmac("sha256", secret)
      .update(\`${t}.${corpsBrut}\`, "utf8")
      .digest("hex"),
    "utf8",
  );
  const recu = Buffer.from(v1, "utf8");
  if (attendu.length !== recu.length) return false;
  return timingSafeEqual(attendu, recu);
}

Em uma rota Next.js, leia o corpo como texto antes de qualquer coisa:

export async function POST(req) {
  const brut = await req.text();
  const ok = verifierSignature(
    process.env.MAILCHEER_WEBHOOK_SECRET,
    req.headers.get("mailcheer-signature"),
    brut,
  );
  if (!ok) return new Response("signature invalide", { status: 401 });
  const evenement = JSON.parse(brut);
  if (evenement.data.test === true) {
    return new Response(null, { status: 204 }); // l'essai
  }
  // ignorez un evenement.id déjà traité,
  // puis traitez evenement.type
  return new Response(null, { status: 204 });
}

Com Express, o mesmo exige express.raw({ type: "application/json" }) nessa rota.

Responder, novas tentativas e duplicatas

Responda 2xx em menos de 15 segundos. Todo o resto é uma falha: outro status, um redirecionamento (não é seguido), nenhuma resposta após 15 segundos, um erro de rede. Faça o trabalho em segundo plano e responda imediatamente — um processamento longo se parece exatamente com uma falha.

Após uma falha, a Mailcheer tenta novamente: 5 tentativas no total. A primeira sai imediatamente, as seguintes 30 s, 2 min, 10 min e 1 h após a falha anterior. As novas tentativas são verificadas a cada 15 segundos: uma delas pode chegar alguns segundos depois. Após a quinta falha, a entrega é failed e não é mais tentada sozinha — e o proprietário do espaço recebe um e-mail que nomeia a assinatura, o evento e a última resposta, no máximo um por assinatura e por dia: reproduza-a em Configurações → API (Reproduzir), ou por POST /api/v1/webhooks/{id}/deliveries/{deliveryId}/replay. GET /api/v1/webhooks/{id}/deliveries lista as entregas: status, tentativas, seu código de resposta, os primeiros 500 caracteres da sua resposta, a próxima tentativa.

O mesmo evento pode chegar mais de uma vez: uma nova tentativa após uma resposta perdida, um replay manual. O id dele — também em Mailcheer-Event-Id — não muda: guarde-o e ignore um id já processado. Mailcheer-Delivery-Id muda com um replay; não pode ser usado para isso.

Os eventos podem chegar fora de ordem: um evento tentado novamente durante uma hora chega depois dos que o seguiram. Ordene-os por created_at.

O evento de teste

POST /api/v1/webhooks/{id}/test — ou o botão Enviar um teste em Configurações → API — envia um evento de teste exatamente pelo mesmo caminho que um real: mesma assinatura, mesmos cabeçalhos, mesmo log, mesmas novas tentativas. A Mailcheer aguarda sua resposta, e a dela informa imediatamente o que houve (status, response_status, error).

⚠️ O evento de teste não carrega nenhum dos campos do evento real. O type dele é o primeiro evento da assinatura, mas o data dele contém apenas test: true e um message:

{
  "id": "evt_Zx9cV8bN7mL6kJ5h",
  "type": "subscriber.unsubscribed",
  "created_at": "2026-09-25T14:07:22.190Z",
  "data": {
    "test": true,
    "message": "Événement d'essai envoyé depuis Mailcheer."
  }
}

Sem email, sem subscriber_id, sem message_id. Verifique data.test === true antes de ler qualquer outra coisa e responda 2xx sem processá-lo. Caso contrário, seu código procura campos ausentes, falha, e o teste parece uma integração quebrada. O message segue o idioma da chamada (Accept-Language) ou da tela de onde o teste partiu.

Os eventos do modo de teste

Um e-mail enviado com uma chave de teste (veja O modo de teste) produz seus eventos como um real: mesmo caminho, mesma assinatura, mesmas novas tentativas. Um único campo os distingue, "test": true no topo do corpo, ao lado de type:

{
  "id": "evt_Qp3rS8tU1vW4xY7z",
  "type": "email.bounced",
  "created_at": "2026-09-25T09:41:07.512Z",
  "test": true,
  "data": {
    "message_id": "cmu7t3kq80004mc0t9e1vx2ab",
    "email": "bounced@simulator.mailcheer.com",
    "source": "api",
    "subject": "Votre facture de septembre",
    "reason": "Permanent",
    "recipient_type": "to"
  }
}

Ao contrário do evento de teste acima, o data dele é exatamente o de um evento real: seu código de processamento roda como em produção, e é exatamente esse o objetivo. Um evento real não tem campo test. Para manter os eventos de teste fora dos seus dados, verifique event.test === true — nunca data.test, que apenas o evento de teste carrega.

O idioma das respostas

As mensagens de erro seguem seu cabeçalho Accept-Language: francês se você pedir, inglês por padrão.

curl https://mailcheer.com/api/v1/me
# {"error":{"code":"missing_api_key","message":"Missing API key. Add the header …"}}

curl https://mailcheer.com/api/v1/me -H "Accept-Language: fr"
# {"error":{"code":"missing_api_key","message":"Clé d'API absente. Ajoutez l'en-tête …"}}

Isso vale para tudo o que um programa lê: as mensagens de erro da API, as ferramentas do servidor MCP (o nome delas, o que fazem, os parâmetros), a referência servida em mailcheer://docs, o mapa GET /api/v1 e o message do evento de teste dos webhooks.

Apenas a primeira preferência é lida: fr-FR,fr;q=0.9,en;q=0.8 pede francês, mesmo que o inglês esteja lá — e en-US,fr;q=0.9 pede inglês.

Uma chave pode ter seu próprio idioma — Configurações → API, Definir. Ela responde quando a chamada não pede nenhum: sem Accept-Language, ou com um cuja primeira preferência não é nem inglês nem francês (*, de-DE …). Um Accept-Language em inglês ou francês sempre prevalece sobre ela. GET /api/v1/me a devolve em key.language.

⚠️ O code, por sua vez, nunca muda de idioma — é contra ele que se escreve a lógica, nunca contra a mensagem.

Os erros

Sempre a mesma forma, com duas leituras possíveis do mesmo conteúdo:

{
  "error": {
    "code": "unverified_from_domain",
    "message": "Le domaine « exemple.fr » n'est pas vérifié dans cet espace. Domaines vérifiés : votredomaine.fr.",
    "details": { "from": "bonjour@exemple.fr", "verifiedDomains": ["votredomaine.fr"] }
  },
  "statusCode": 422,
  "message": "Le domaine « exemple.fr » n'est pas vérifié dans cet espace. Domaines vérifiés : votredomaine.fr.",
  "name": "unverified_from_domain"
}

error é a forma da Mailcheer: estruturada, com em details o que é necessário para corrigir. Os três campos achatados — statusCode, message, name — são os da Resend, e estão lá para que código escrito contra a API antiga exiba uma mensagem correta sem ser relido.

Escreva sua lógica contra code (ou name, é o mesmo valor), nunca contra message. A mensagem é feita para ser lida por um humano, e nos permitimos reformulá-la.

CódigoStatusO que significa
missing_api_key401Nenhum cabeçalho Authorization.
invalid_api_key401Chave desconhecida.
revoked_api_key401Chave cortada nas configurações.
insufficient_scope403A chave não tem a permissão solicitada.
reputation_blocked403Seus envios estão bloqueados: muitos bounces ou reclamações.
sending_blocked403Os envios do espaço estão interrompidos (suspenso, bloqueado ou banido): nada sai, por nenhuma via. details.reason diz qual.
commitment_required403O compromisso antispam não foi aceito: ele se abre no primeiro envio do espaço, ou na página /fr/engagement.
sending_paused423O espaço está em pausa para uma revisão de segurança. A chamada não tem nada de errado: reenvie-a como está após a decisão. Retry-After dá um intervalo para tentar novamente; enquanto a revisão durar, a chamada responde novamente 423 e não escreve nada.
daily_quota_exceeded429Um espaço novo atingiu o limite do dia: 100 e-mails por 24 horas corridas durante os três primeiros dias, 500 até o sétimo. Retry-After e details dizem quando tentar novamente.
quota_exceeded402A cota mensal do plano não cobre o envio: nada saiu. details dá plan, quota, sent, in_flight, remaining, requested e resets_at.
free_plan_domain_used402Espaço gratuito: o domínio de envio já usou a oferta gratuita este mês em outro espaço. Nada saiu. details dá domain, root_domain, period e resets_at.
key_quota_exceeded402Esta chave atingiu o próprio limite mensal (Configurações → API); o espaço, por sua vez, pode ainda ter espaço. Nada saiu. details dá key_id, key_name, limit, used, remaining, requested e resets_at.
not_found404O objeto não existe neste espaço.
conflict409Estado incompatível: campanha já enviada, assinante removido.
idempotency_key_reused409Mesmo Idempotency-Key, corpo diferente.
idempotency_key_in_use409Uma chamada idêntica, mesmo Idempotency-Key, ainda está em andamento. Reenvie-a como está após Retry-After: você recebe a resposta dela, sem segundo envio.
validation_error422Um campo está ausente ou mal formatado.
unverified_from_domain422O domínio de from não está verificado.
suppressed_recipient422Um destinatário foi descartado: bounce, reclamação, remoção manual, ou cancelamento se o envio carregar unsubscribe_url.
rate_limit_exceeded429Mais de 600 requisições por minuto — um lote de N assinantes conta como N (veja Retry-After).
send_failed502Nosso provedor de envio recusou a mensagem.
internal_error500Uma falha do nosso lado.

Todo 429 e todo 423 carregam Retry-After, em segundos: aguarde esse intervalo antes de reenviar a mesma chamada.

Limite de taxa

600 requisições por minuto e por chave. Cada resposta carrega X-RateLimit-Limit, X-RateLimit-Remaining e X-RateLimit-Reset; uma recusa carrega também Retry-After, em segundos. Um lote de N assinantes (POST /api/v1/subscribers/batch) conta como N requisições. Não confunda esses cabeçalhos com os da cota do mês (Mailcheer-Quota-*, acima): a taxa é contada por minuto, a cota por mês.

Precisa de mais? Escreva para nós: analisamos seu caso em vez de deixar você tentar em loop.

O servidor MCP

MCP — Model Context Protocol — é a forma como um agente de IA descobre as ferramentas de um software e as utiliza. O Mailcheer expõe um servidor MCP hospedado: nada para instalar, um endereço e sua chave no cabeçalho Authorization.

Claude Code

claude mcp add --transport http mailcheer https://mailcheer.com/api/mcp \
  --header "Authorization: Bearer mch_live_…"

Adicione --scope user para que a conexão valha em todos os seus projetos, e não apenas na pasta atual.

Codex — ele lê a chave de uma variável de ambiente e a envia ele mesmo no cabeçalho Authorization:

export MAILCHEER_API_KEY="mch_live_…"
codex mcp add mailcheer \
  --url https://mailcheer.com/api/mcp \
  --bearer-token-env-var MAILCHEER_API_KEY

O comando grava isto em ~/.codex/config.toml, que você também pode escrever manualmente:

[mcp_servers.mailcheer]
url = "https://mailcheer.com/api/mcp"
bearer_token_env_var = "MAILCHEER_API_KEY"

Cursor — em .cursor/mcp.json (um projeto) ou ~/.cursor/mcp.json (todos os seus projetos):

{
  "mcpServers": {
    "mailcheer": {
      "url": "https://mailcheer.com/api/mcp",
      "headers": { "Authorization": "Bearer mch_live_…" }
    }
  }
}

Em seguida, no seu agente: "Qual espaço do Mailcheer você vê e quais domínios de envio estão verificados?". Ele chamará get_account, que não modifica nada — é a forma correta de verificar uma conexão.

As ferramentas expostas

FerramentaO que ela faz
get_accountO espaço, as permissões, o consumo do mês (incluindo fila e redefinição), o pagamento, os limites, os domínios verificados.
send_emailEnvia um e-mail unitário. Irreversível.
get_emailO estado de um e-mail enviado.
list_subscribersLista os assinantes, página por página.
add_subscriberAdiciona ou atualiza um assinante.
remove_subscriberCancela a inscrição e descarta o endereço. Irreversível.
list_tagsAs etiquetas, com o número de assinantes que possuem cada uma.
tag_subscriberAplica e remove etiquetas em um assinante, sem inscrição ou e-mail.
erase_subscriberApaga uma pessoa a pedido dela (LGPD). Irreversível.
list_suppressionOs endereços descartados, com o motivo.
add_suppressionDescarta um endereço. Irreversível.
list_campaignsAs campanhas do espaço.
create_campaignCria um rascunho. Nada é enviado.
update_campaignModifica um rascunho. Nada é enviado.
preview_campaign_sendDiz se a campanha seria enviada e para quantas pessoas, endereço por endereço com to. Nada é enviado.
send_campaignEnvia para todos os assinantes ativos, ou apenas para os assinantes ativos entre os endereços de to. Irreversível.
get_campaign_statsNúmeros e estado de uma campanha.

Cada ferramenta é uma chamada à API acima, nada mais: mesmos direitos, mesma cota, mesma lista de supressão, mesmas recusas. Um segundo caminho de acesso com lógica própria seria um segundo conjunto de regras, e no dia em que um dos dois mudasse, o MCP se tornaria a porta dos fundos.

Nenhuma ferramenta remove um endereço da lista de supressão. É a única ação do produto que suspende uma capacidade de envio, e um agente ao qual se dissesse "limpe a lista" o faria sem hesitar. Isso é feito manualmente, no seu espaço.

O recurso mailcheer://docs dá ao agente a referência completa: ele não precisa conhecê-la antecipadamente.

O SDK Node.js

O pacote oficial para Node.js e TypeScript encapsula esta API: tipos para cada chamada e cada evento, códigos de erro estáveis, novas tentativas que nunca enviam duas vezes, a cota lida em cada resposta e a assinatura dos webhooks verificada em uma linha. Sem dependências; ESM e CommonJS. mailcheer no npm.

npm install mailcheer   # Node.js 20+, Bun, Deno
import { Mailcheer } from "mailcheer";

const mailcheer = new Mailcheer(process.env.MAILCHEER_API_KEY, { language: "fr" });

const { data, error, meta } = await mailcheer.emails.send(
  {
    from: "Ma Boutique <factures@ma-boutique.fr>",
    to: "client@exemple.fr",
    subject: "Votre facture de septembre",
    html: "<p>La voici.</p>",
  },
  { idempotencyKey: "facture-2026-0042" },
);
if (error) console.error(error.code, error.message);
else console.log(data.id, meta.quota?.remaining);

O que ele faz por você:

  • Novas tentativas que nunca enviam duas vezes. Após uma falha de rede, um tempo limite excedido, um 429 ou um 5xx, o SDK tenta novamente — duas vezes por padrão — e aguarda Retry-After quando a API o fornece. Uma chamada que modifica algo só é tentada novamente se tiver uma chave anti-duplicação, ou se a API a recusou antes de lê-la (rate_limit_exceeded).
  • Erros nos quais ancorar seu código. Cada método retorna { data, error, meta } e nunca lança exceção; error é um MailcheerError que carrega o code estável, os details e retryAfter. Mensagens em inglês por padrão, em francês com language: "fr".
  • Sua cota. meta.quota e mailcheer.lastQuota mantêm os números Mailcheer-Quota-*; meta.keyQuota os de uma chave com teto mensal próprio; meta.mode vale test para uma chave de teste (mch_test_…).
  • Os webhooks. constructWebhookEvent(secret, entete, corpsBrut) verifica a assinatura e a janela de cinco minutos exatamente como descrito em Verificar a assinatura e, em seguida, retorna o evento, tipado de acordo com seu type. Em um ambiente "edge" sem node:crypto, constructWebhookEventAsync() faz o mesmo com Web Crypto.
  • Todas as rotas desta página — e-mails, assinantes e lotes, etiquetas, lista de supressão, campanhas e rascunhos, webhooks — e request() para uma rota mais recente que a versão que você usa.

O SDK Python

O pacote oficial para Python encapsula a mesma API: um cliente bloqueante e um cliente asyncio com os mesmos métodos, respostas e erros tipados, novas tentativas que nunca enviam duas vezes, a cota lida em cada resposta e a assinatura dos webhooks verificada em uma linha. Uma única dependência, httpx. mailcheer no PyPI.

pip install mailcheer   # Python 3.9+
from mailcheer import Mailcheer, MailcheerError

mailcheer = Mailcheer(language="fr")  # lit la variable MAILCHEER_API_KEY

try:
    email = mailcheer.emails.send(
        {
            "from": "Ma Boutique <factures@ma-boutique.fr>",
            "to": "client@exemple.fr",
            "subject": "Votre facture de septembre",
            "html": "<p>La voici.</p>",
        },
        idempotency_key="facture-2026-0042",
    )
    print(email["id"], mailcheer.last_quota)
except MailcheerError as error:
    print(error.code, error.message)

O que ele faz por você:

  • Novas tentativas que nunca enviam duas vezes. Após uma falha de rede, um tempo limite excedido, um 429 ou um 5xx, o SDK tenta novamente — duas vezes por padrão (max_retries) — e aguarda Retry-After quando a API o fornece. Uma chamada que modifica algo só é tentada novamente se tiver uma chave anti-duplicação, ou se a API a recusou antes de lê-la (rate_limit_exceeded).
  • Uma exceção por família de recusa. Ao contrário do SDK Node.js, um método lança exceção: MailcheerError, ou a subclasse de seu status — AuthenticationError (401), QuotaExceededError (402), ValidationError (422), RateLimitError (429)… Cada uma carrega o code estável, os details e retry_after. Mensagens em inglês por padrão, em francês com language="fr".
  • Sua cota. mailcheer.last_quota mantém os números Mailcheer-Quota-*; mailcheer.last_key_quota os de uma chave com teto mensal próprio; mailcheer.last_response.mode vale "test" para uma chave de teste (mch_test_…).
  • asyncio. AsyncMailcheer tem os mesmos métodos, para aguardar com await; list_all() percorre todas as páginas, com for ou async for.
  • Todas as rotas desta página — e-mails, assinantes e lotes, etiquetas, lista de supressão, campanhas e rascunhos, webhooks — e request() para uma rota mais recente que a versão que você usa.

Para os webhooks, construct_webhook_event() verifica a assinatura e a janela de cinco minutos exatamente como descrito em Verificar a assinatura e, em seguida, retorna o evento — ou lança WebhookSignatureError. Dê a ele o corpo bruto, nunca o JSON decodificado, que ele recusa: request.get_data() com Flask, request.body com Django, await request.body() com FastAPI.

from mailcheer import WebhookSignatureError, construct_webhook_event, is_test_event

@app.post("/webhooks/mailcheer")  # Flask
def webhook_mailcheer():
    try:
        evenement = construct_webhook_event(
            os.environ["MAILCHEER_WEBHOOK_SECRET"],
            request.headers.get("Mailcheer-Signature"),
            request.get_data(),
        )
    except WebhookSignatureError:
        return "signature invalide", 401
    if is_test_event(evenement):
        return "", 204  # l'essai
    # ignorez un evenement["id"] déjà traité, puis traitez evenement["type"]
    return "", 204

O que muda se você vem do Resend

Os campos de POST /v1/emails e a resposta { id } são os mesmos. Na prática: o endereço base e a chave.

Três formas de migrar.

Com o SDK — npm install mailcheer: a mesma forma { data, error } que o SDK do Resend, com adicionalmente os tipos, as novas tentativas e a cota (veja O SDK Node.js).

- const resend = new Resend(process.env.RESEND_API_KEY);
+ const mailcheer = new Mailcheer(process.env.MAILCHEER_API_KEY);

Com o cliente mínimo — um arquivo para copiar, sem dependências, a mesma assinatura que o SDK do Resend. Baixe-o: mailcheer.com/mailcheer-client.fr.ts — a versão em inglês, canônica, está em /mailcheer-client.ts.

// avant
const resend = new Resend(process.env.RESEND_API_KEY);

// après
const mailcheer = new Mailcheer(process.env.MAILCHEER_API_KEY);

// le reste de votre code ne bouge pas
const { data, error } = await mailcheer.emails.send({ from, to, subject, html, text });
if (error) throw new Error(\`Email delivery failed: ${error.message}\`);
return { providerId: data?.id ?? null };

Ele nunca lança exceção: uma falha de rede também se torna um error, com name: "network_error". Isso é intencional — um método que lançasse exceção onde o antigo retornava um objeto transformaria "mudar duas linhas" em "reler cada chamada", e as chamadas que esquecemos de reler são justamente os caminhos de erro.

Sem copiar nada — um fetch puro basta:

const res = await fetch("https://mailcheer.com/api/v1/emails", {
  method: "POST",
  headers: {
    Authorization: \`Bearer ${process.env.MAILCHEER_API_KEY}\`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ from, to, subject, html }),
});
const body = await res.json();
if (!res.ok) throw new Error(body.message); // le message est lisible par un humain
const id = body.id;

Três pontos a conhecer:

  • O domínio de from deve ser verificado no seu espaço Mailcheer, não no Resend. Adicione-o em Domínios e configure os registros DNS.
  • Um único espaço basta para seus e-mails transacionais e sua newsletter. Um cancelamento de inscrição da newsletter só interrompe os envios em massa: suas faturas e redefinições de senha sempre chegam a esse endereço. Um bounce ou uma reclamação interrompem tudo. Se você também enviar uma carta pela API, passe unsubscribe_url: um cancelado não a receberá.
  • A cota mensal é compartilhada com suas campanhas.

Onde esses e-mails vivem

Os e-mails enviados pela API não se juntam às suas campanhas: eles vivem à parte, e isso não é um detalhe técnico.

Um destinatário de fatura não é um assinante. Colocá-lo junto com seus assinantes o teria inscrito na sua lista sem que ele jamais tivesse consentido em receber sua newsletter — contabilizado no seu painel e segmentado pela sua próxima campanha. Seus números de assinantes permanecem, portanto, os de seus assinantes reais.

O que é compartilhado, por outro lado: a cota mensal, a lista de supressão e o monitoramento de bounces. São as três coisas que comprometem sua reputação de remetente, e ela é a mesma dos dois lados.

A descrição técnica

O arquivo OpenAPI 3.1 é servido como está, em francês: mailcheer.com/openapi.fr.json — a versão em inglês, canônica, está em /openapi.json. Ele descreve cada endpoint, cada campo e cada erro — o suficiente para gerar um cliente na sua linguagem ou entregá-lo a um agente.