Automação de e-mail com Claude Code: de leads à monetização
Implemente automação de e-mail com Claude Code: lead magnet, consentimento, retries, analytics e receita.
Automação de e-mail não é apenas disparar uma mensagem depois de um formulário. Um fluxo que sustenta receita entrega o lead magnet, inicia onboarding, acompanha pedidos de consultoria, registra consentimento, processa descadastro, remove endereços com bounce, tenta novamente falhas temporárias e mostra quais CTAs geram compra, treinamento ou conversa comercial.
Claude Code ajuda porque e-mail cruza várias partes do sistema: schemas, templates, adaptadores de provedor, fila, webhooks, eventos de analytics e documentação. Ao revisar o funil do PDF gratuito deste site, o primeiro erro foi gerar só uma função de envio com Resend. Ela funcionava, mas consentimento, URL de descadastro, tratamento de bounce e análise de CTA ficaram como remendos. O caminho melhor é pedir um plano limitado, confirmar os arquivos e só então implementar.
Este guia monta uma base Node.js/TypeScript independente de provedor, alternando APIs no estilo Resend e SendGrid. Vamos cobrir entrega de lead magnet, sequência de onboarding, follow-up de consultoria, noções de SPF/DKIM/DMARC, limites seguros de outreach, rate limits, queue/retry, templates, analytics e CTA para produtos, treinamento e consultoria. Para o funil comercial, veja também auditoria de funil de conteúdo, implementação de analytics e gestão de cookies e consentimento.
Desenhe antes de codar
Lead magnet é um recurso gratuito, como PDF, checklist ou template, entregue em troca do e-mail. Onboarding é a sequência que ajuda alguém a começar depois de se cadastrar, comprar ou entrar em um treinamento. Follow-up de consultoria é o e-mail operacional com resumo da conversa, próximos passos, proposta ou link de agenda.
Não coloque tudo em uma newsletter genérica. Consentimento, tom, métrica e risco são diferentes.
| Objetivo | Destinatário | Exemplo | Caminho de receita | Risco |
|---|---|---|---|---|
| Captura de lead | Leitor que pediu PDF | Link de download e guia relacionado | PDF grátis para produtos | Salvar consentimento e URL de descadastro |
| Onboarding | Cliente ou aluno | Guia inicial, checklist, bloqueios comuns | Templates, curso, suporte | Não transformar recibo em venda agressiva |
| Consultoria | Lead qualificado | Notas, proposta, próxima agenda | Treinamento e consultoria | Personalizar com o contexto real |
| Reengajamento | Leitor consentido, mas inativo | Caso prático ou grande atualização | Produto ou consulta | Monitorar frequência, bounces e opt-outs |
Termos técnicos precisam ficar claros. SPF é um registro DNS que informa quais servidores podem enviar e-mail pelo seu domínio. DKIM adiciona uma assinatura para verificar autorização e integridade. DMARC define a política quando SPF ou DKIM não alinham. Bounce é falha de entrega. Rate limit é quando o provedor reduz ou rejeita requisições porque você enviou rápido demais, bateu cota ou precisa proteger reputação.
Use documentos oficiais como referência. Para Gmail, veja Google email sender guidelines. Para provedores, comece em Resend domain management ou Twilio SendGrid domain authentication. DMARC foi atualizado em 2026 pelo RFC 9989, que substitui o antigo RFC 7489. Para e-mail comercial nos EUA, confira o guia CAN-SPAM da FTC. Isto é orientação técnica, não aconselhamento jurídico.
flowchart LR
Visitor["Leitor"]
Form["Formulário de lead"]
Consent["Registro de consentimento"]
Queue["Fila de e-mail"]
Provider["Resend / SendGrid"]
Inbox["Caixa de entrada"]
Webhook["Eventos"]
Analytics["Analytics"]
Offer["Produto / treinamento / consultoria"]
Visitor --> Form --> Consent --> Queue --> Provider --> Inbox
Provider --> Webhook --> Analytics --> Offer
Inbox --> Offer
Prompt para Claude Code
Um pedido vago gera só uma função de envio. Um pedido bom define objetivo, fronteiras e verificação.
Implemente automação de e-mail neste repositório.
Objetivos: entrega de lead magnet, sequência de onboarding com 3 e-mails e follow-up de consultoria.
Restrições:
- Usar Node.js 20+ e TypeScript.
- Criar adapter para alternar entre API estilo Resend e estilo SendGrid.
- API keys só no servidor, via variáveis de ambiente.
- Criar schemas para lead, email job, unsubscribe e provider event.
- Repetir 429 e 5xx com exponential backoff.
- Não enviar para endereços com unsubscribe, complaint ou suppression.
- Colocar hard bounces repetidos em suppression list.
- Incluir texto, HTML, URL de descadastro e remetente claro.
- Documentar links oficiais de provedor e autenticação.
- Adicionar scripts executáveis e testes focados.
Primeiro mostre tabela de design e lista de arquivos. Aguarde aprovação antes de editar.
Starter copiável
O exemplo usa um JSON local como fila para rodar em demo. Em produção, troque por Postgres, Redis, SQS, Cloud Tasks ou outra fila durável com lock e auditoria.
{
"type": "module",
"scripts": {
"lead:send": "tsx scripts/send-lead-magnet.ts",
"email:worker": "tsx scripts/email-worker.ts"
},
"dependencies": {
"zod": "latest"
},
"devDependencies": {
"@types/node": "latest",
"tsx": "latest",
"typescript": "latest"
}
}
// src/email/schema.ts
import { z } from "zod";
export const leadSchema = z.object({
email: z.string().email(),
name: z.string().trim().min(1).max(80),
locale: z.enum(["ja", "en", "zh", "ko", "es", "fr", "de", "pt", "hi", "id"]).default("pt"),
source: z.enum(["article", "product", "workshop", "consultation"]),
consentAt: z.string().datetime(),
tags: z.array(z.string()).default([]),
});
export const sendMessageSchema = z.object({
to: z.string().email(),
from: z.string().email(),
fromName: z.string().min(1),
replyTo: z.string().email().optional(),
subject: z.string().min(1).max(120),
text: z.string().min(1),
html: z.string().min(1),
unsubscribeUrl: z.string().url(),
category: z.enum(["lead_magnet", "onboarding", "consultation_followup"]),
metadata: z.record(z.string()).default({}),
});
export const emailJobSchema = z.object({
message: sendMessageSchema,
maxAttempts: z.number().int().min(1).max(8).default(4),
});
export type SendMessage = z.infer<typeof sendMessageSchema>;
export type EmailJobInput = z.infer<typeof emailJobSchema>;
// src/email/provider.ts
import { randomUUID } from "node:crypto";
import type { SendMessage } from "./schema";
type SendResult = { providerMessageId: string; acceptedAt: string };
export interface EmailProvider { send(message: SendMessage): Promise<SendResult>; }
function requiredEnv(name: string): string {
const value = process.env[name];
if (!value) throw new Error(`Missing env: ${name}`);
return value;
}
async function parseProviderError(response: Response): Promise<Error> {
const body = await response.text().catch(() => "");
const retryable = response.status === 429 || response.status >= 500;
const error = new Error(`Email provider error ${response.status}: ${body || response.statusText}`);
(error as Error & { retryable?: boolean }).retryable = retryable;
return error;
}
export class ResendProvider implements EmailProvider {
async send(message: SendMessage): Promise<SendResult> {
const response = await fetch("https://api.resend.com/emails", {
method: "POST",
headers: {
Authorization: `Bearer ${requiredEnv("RESEND_API_KEY")}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
from: `${message.fromName} <${message.from}>`,
to: [message.to],
reply_to: message.replyTo,
subject: message.subject,
text: message.text,
html: message.html,
headers: {
"List-Unsubscribe": `<${message.unsubscribeUrl}>`,
"List-Unsubscribe-Post": "List-Unsubscribe=One-Click",
},
}),
});
if (!response.ok) throw await parseProviderError(response);
const data = (await response.json().catch(() => ({}))) as { id?: string };
return { providerMessageId: data.id ?? randomUUID(), acceptedAt: new Date().toISOString() };
}
}
export class SendGridProvider implements EmailProvider {
async send(message: SendMessage): Promise<SendResult> {
const response = await fetch("https://api.sendgrid.com/v3/mail/send", {
method: "POST",
headers: {
Authorization: `Bearer ${requiredEnv("SENDGRID_API_KEY")}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
personalizations: [{ to: [{ email: message.to }], custom_args: message.metadata }],
from: { email: message.from, name: message.fromName },
reply_to: message.replyTo ? { email: message.replyTo } : undefined,
subject: message.subject,
content: [
{ type: "text/plain", value: message.text },
{ type: "text/html", value: message.html },
],
headers: {
"List-Unsubscribe": `<${message.unsubscribeUrl}>`,
"List-Unsubscribe-Post": "List-Unsubscribe=One-Click",
},
}),
});
if (!response.ok) throw await parseProviderError(response);
return { providerMessageId: response.headers.get("x-message-id") ?? randomUUID(), acceptedAt: new Date().toISOString() };
}
}
export function createEmailProvider(): EmailProvider {
return process.env.EMAIL_PROVIDER === "sendgrid" ? new SendGridProvider() : new ResendProvider();
}
// src/email/queue.ts
import { readFile, writeFile } from "node:fs/promises";
import { existsSync } from "node:fs";
import { randomUUID } from "node:crypto";
import { emailJobSchema, type EmailJobInput } from "./schema";
type StoredJob = EmailJobInput & {
id: string;
status: "scheduled" | "processing" | "sent" | "failed";
attempts: number;
nextAttemptAt: string;
lastError?: string;
};
const queueFile = process.env.EMAIL_QUEUE_FILE ?? ".email-queue.json";
async function loadQueue(): Promise<StoredJob[]> {
if (!existsSync(queueFile)) return [];
return JSON.parse(await readFile(queueFile, "utf8")) as StoredJob[];
}
async function saveQueue(jobs: StoredJob[]) {
await writeFile(queueFile, JSON.stringify(jobs, null, 2) + "\n");
}
export async function enqueueEmail(input: EmailJobInput) {
const parsed = emailJobSchema.parse(input);
const jobs = await loadQueue();
const job: StoredJob = { ...parsed, id: randomUUID(), status: "scheduled", attempts: 0, nextAttemptAt: new Date().toISOString() };
jobs.push(job);
await saveQueue(jobs);
return job.id;
}
export async function claimDueJobs(limit = 5): Promise<StoredJob[]> {
const now = Date.now();
const jobs = await loadQueue();
const due = jobs.filter((job) => job.status === "scheduled" && Date.parse(job.nextAttemptAt) <= now).slice(0, limit);
for (const job of due) job.status = "processing";
await saveQueue(jobs);
return due;
}
export async function completeJob(id: string) {
const jobs = await loadQueue();
const job = jobs.find((item) => item.id === id);
if (job) job.status = "sent";
await saveQueue(jobs);
}
export async function failJob(id: string, error: unknown) {
const jobs = await loadQueue();
const job = jobs.find((item) => item.id === id);
if (!job) return;
job.attempts += 1;
job.lastError = error instanceof Error ? error.message : String(error);
if (job.attempts >= job.maxAttempts) {
job.status = "failed";
} else {
const delayMs = Math.min(15 * 60_000, 2 ** job.attempts * 1000);
job.status = "scheduled";
job.nextAttemptAt = new Date(Date.now() + delayMs).toISOString();
}
await saveQueue(jobs);
}
// scripts/email-worker.ts
import { claimDueJobs, completeJob, failJob } from "../src/email/queue";
import { createEmailProvider } from "../src/email/provider";
const provider = createEmailProvider();
const jobs = await claimDueJobs(Number(process.env.EMAIL_WORKER_BATCH ?? 3));
for (const job of jobs) {
try {
const result = await provider.send(job.message);
await completeJob(job.id);
console.log(`sent ${job.id} as ${result.providerMessageId}`);
} catch (error) {
await failJob(job.id, error);
console.error(`failed ${job.id}`, error);
}
}
Teste primeiro com um endereço seu. Não envie a leitores reais antes de autenticar o domínio e validar o descadastro.
npm install
[email protected] APP_URL=https://example.com npm run lead:send
EMAIL_PROVIDER=resend RESEND_API_KEY=re_xxx npm run email:worker
Bounces, descadastros e analytics
A resposta positiva do provedor só diz que a requisição foi aceita. Não prova leitura, clique ou interesse. Normalize webhooks para um modelo interno e use suppression list para bounce duro, complaint e unsubscribe.
// src/email/events.ts
import { z } from "zod";
const providerEventSchema = z.object({
provider: z.enum(["resend", "sendgrid", "unknown"]),
type: z.enum(["delivered", "bounce", "complaint", "unsubscribe", "open", "click", "deferred"]),
email: z.string().email().optional(),
providerMessageId: z.string().optional(),
reason: z.string().optional(),
occurredAt: z.string().datetime(),
});
export function normalizeProviderEvent(payload: unknown) {
const raw = payload as Record<string, unknown>;
const type = String(raw.type ?? raw.event ?? "delivered");
const mappedType =
type.includes("bounce") ? "bounce" :
type.includes("complaint") || type.includes("spam") ? "complaint" :
type.includes("unsubscribe") ? "unsubscribe" :
type.includes("click") ? "click" :
type.includes("open") ? "open" :
type.includes("defer") ? "deferred" :
"delivered";
return providerEventSchema.parse({
provider: raw.sg_event_id ? "sendgrid" : raw.created_at ? "resend" : "unknown",
type: mappedType,
email: String(raw.email ?? raw.recipient ?? "") || undefined,
providerMessageId: String(raw.email_id ?? raw.sg_message_id ?? ""),
reason: typeof raw.reason === "string" ? raw.reason : undefined,
occurredAt: new Date(String(raw.created_at ?? Date.now())).toISOString(),
});
}
Não dependa só de abertura. Bloqueio de imagem e privacidade distorcem esse número. Meça download, clique em CTA, início de formulário de consultoria, resposta, descadastro, bounce e compra. Use eventos como lead_magnet_requested, email_cta_click e consultation_request_started.
Casos de uso
Primeiro: PDF gratuito no fim de artigos técnicos. Envie o download imediatamente, depois um erro comum de configuração, depois um template de produto e por fim convite para treinamento ou consultoria. Cada e-mail deve ter uma ação principal e link de descadastro.
Segundo: onboarding após compra. Quem comprou um guia ou entrou em workshop precisa começar, resolver bloqueios e ver uso avançado. Ajudar o comprador a ter sucesso é melhor que transformar recibo em propaganda.
Terceiro: follow-up de consultoria. Inclua notas da reunião, decisões, próximos passos, links, prazo e CTA de agenda ou proposta. Se não refletir a conversa real, parecerá spam.
Quarto: reengajamento de baixa frequência. Para leitores consentidos, mas inativos, envie só grandes atualizações, histórias de falha úteis ou novos recursos. Se cliques e respostas não voltarem, reduza ou pare.
Falhas comuns
A primeira falha é expor a API key no navegador. Envio de e-mail deve ficar no servidor.
A segunda é enviar sem autenticar domínio. Configure SPF, DKIM e DMARC.
A terceira é ignorar descadastro e bounce. Unsubscribe, complaint e hard bounce devem sair das campanhas comuns.
A quarta é repetir imediatamente após rate limit. Trate 429 e 5xx temporários com backoff. Limites exatos variam por conta, plano, reputação e destinatário.
A quinta é misturar transacional e promocional. Reset de senha, recibo e alerta de conta devem ser claros. CTAs comerciais pertencem a mensagens com consentimento e contexto.
CTA de monetização
O sistema está pronto quando o leitor consegue escolher o próximo passo naturalmente. No ClaudeCodeLab, iniciantes começam pelo PDF grátis, builders veem produtos e templates, e equipes usam treinamento e consultoria para aplicar isso a um repositório real.
Na prática, o maior ganho veio de desenhar consentimento, descadastro, bounce e analytics de CTA antes do código do provedor. Comece com um único e-mail de lead magnet, valide envio, descadastro, bounce e clique, e depois expanda para onboarding e consultoria.
PDF grátis: cheatsheet do Claude Code
Informe seu e-mail e baixe uma página com comandos, hábitos de revisão e workflows seguros.
Cuidamos dos seus dados e não enviamos spam.
Sobre o autor
Masa
Engenheiro focado em workflows práticos com Claude Code.
Artigos relacionados
Checklist de permissões antes de Claude Code editar site de cliente
Um quadro para agências usarem IA em landing pages sem tocar áreas sensíveis.
Transforme tickets de suporte SaaS em passos reproduzíveis com Claude Code
Fluxo para converter chamados vagos em reprodução, evidência e nota útil para engenharia.
Rotina de 10 minutos para transformar notas antigas do Obsidian em brief para o Claude Code
Suas notas do Obsidian viram lixo toda sessão? Separe fatos, decisões e dúvidas e transforme-as num brief que o Claude Code executa direto.