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_TO=you@example.com 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
Workflow Obsidian para CLAUDE.md com Claude Code
Transforme notas de trabalho do Obsidian em notas operacionais CLAUDE.md para preservar contexto.
Claude Code Revenue CTA Routing: artigos para PDF, Gumroad e consultoria
Um fluxo com Claude Code para levar leitores ao PDF grátis, Gumroad ou consultoria conforme intenção.
Regras de handoff para equipes com Claude Code: evidências, permissões, rollback e receita
Formato prático para entregar trabalho do Claude Code com prova, permissões, rollback, PDF grátis, Gumroad e consultoria.