Use Cases (Actualizado: 2/6/2026)

Automatización de emails con Claude Code: de leads a monetización

Implementa automatización de emails con Claude Code: lead magnets, consentimiento, reintentos, analítica y ventas.

Automatización de emails con Claude Code: de leads a monetización

La automatización de emails no es solo enviar un mensaje después de que alguien complete un formulario. Un flujo que genera ingresos entrega el lead magnet, inicia una secuencia de onboarding, hace seguimiento a una consulta, guarda el consentimiento, procesa bajas, deja de escribir a direcciones con rebotes, reintenta fallos temporales y mide qué CTA termina en compra, formación o consultoría.

Claude Code encaja bien porque el email toca muchas piezas a la vez: esquemas, plantillas, adaptadores de proveedor, colas, webhooks, eventos de analítica y documentación. En una revisión del funnel de PDF gratuito de este sitio, el error inicial fue pedir solo una función de envío con Resend. Funcionaba, pero el consentimiento, la URL de baja, los rebotes y la analítica de CTA quedaron como parches. Es mejor pedir primero una tabla de diseño y una lista de archivos, limitar el alcance y después dejar que Claude Code implemente.

En esta guía construiremos una base Node.js/TypeScript independiente del proveedor. Podrás alternar una API estilo Resend o estilo SendGrid, incluir límites de envío, cola y reintentos, plantillas, eventos de entrega, analítica y un camino comercial hacia productos, formación o consultoría. Para el contexto de monetización, revisa también auditoría del funnel de contenidos, implementación de analítica y gestión de cookies y consentimiento.

Diseña antes de escribir código

Un lead magnet es un recurso gratuito, como un PDF, checklist o plantilla, que se entrega a cambio de un email. Onboarding es la secuencia que ayuda a una persona a empezar después de registrarse, comprar o apuntarse a una formación. Seguimiento de consultoría es el email operativo que resume una conversación, comparte próximos pasos o propone una fecha.

No metas todo en una etiqueta genérica de newsletter. Cada tipo tiene consentimiento, tono, métricas y riesgos distintos.

ObjetivoDestinatarioEmail ejemploRuta comercialRiesgo a controlar
Captura de leadsLector que pidió un PDFEnlace de descarga y guía relacionadaPDF gratuito a productosGuardar consentimiento y enlace de baja
OnboardingCliente o alumnoGuía inicial, checklist, bloqueos frecuentesPlantillas, curso, soporteNo convertir recibos en ventas agresivas
SeguimientoLead cualificadoNotas, propuesta, próxima reservaFormación y consultoríaPersonalizar según la conversación
ReactivaciónLector con consentimiento pero inactivoCaso práctico o actualización importanteProducto o consultaVigilar frecuencia, bajas y rebotes

También conviene explicar los términos. SPF es un registro DNS que indica qué servidores pueden enviar en nombre de tu dominio. DKIM añade una firma para demostrar que el mensaje fue autorizado y no se modificó. DMARC indica qué política aplicar si SPF o DKIM no se alinean. Un rebote es una entrega fallida. Un límite de tasa aparece cuando el proveedor reduce o rechaza solicitudes porque envías demasiado rápido, llegas a una cuota o tu reputación necesita cuidado.

Usa documentación oficial como referencia. Para Gmail, consulta Google email sender guidelines. Para proveedores, empieza por Resend domain management o Twilio SendGrid domain authentication. DMARC fue actualizado en 2026 por RFC 9989, que reemplaza al antiguo RFC 7489. Para email comercial en EE. UU., revisa la guía CAN-SPAM de la FTC. Esto es guía técnica, no asesoría legal.

flowchart LR
  Visitor["Lector"]
  Form["Formulario de lead"]
  Consent["Registro de consentimiento"]
  Queue["Cola de email"]
  Provider["Resend / SendGrid"]
  Inbox["Bandeja de entrada"]
  Webhook["Eventos de entrega"]
  Analytics["Analítica"]
  Offer["Producto / formación / consultoría"]

  Visitor --> Form --> Consent --> Queue --> Provider --> Inbox
  Provider --> Webhook --> Analytics --> Offer
  Inbox --> Offer

Prompt para Claude Code

Un prompt vago produce una función de envío. Un prompt útil define objetivos, límites y verificación.

Implementa automatización de emails en este repositorio.
Los objetivos son entregar un lead magnet, una secuencia de onboarding de 3 emails y seguimiento de consultoría.

Restricciones:
- Usar Node.js 20+ y TypeScript.
- Crear un adapter que cambie entre API estilo Resend y API estilo SendGrid.
- Mantener API keys solo en variables de entorno del servidor.
- Crear schemas para lead, email job, unsubscribe y provider event.
- Reintentar 429 y 5xx con exponential backoff.
- No enviar a direcciones con unsubscribe, complaint o suppression.
- Añadir hard bounces repetidos a suppression list.
- Incluir versión texto, HTML, URL de baja y remitente claro.
- Documentar enlaces oficiales de proveedor y autenticación.
- Añadir scripts ejecutables y pruebas enfocadas.

Primero muestra tabla de diseño y lista de archivos. Espera aprobación antes de editar.

Implementación mínima copiable

Este starter usa un JSON local como cola para que puedas ejecutarlo en un proyecto de prueba. En producción, cámbialo por Postgres, Redis, SQS, Cloud Tasks u otra cola durable con bloqueo y auditoría.

{
  "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("es"),
  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);
  }
}

Prueba primero con una dirección que controles. No envíes a lectores reales hasta autenticar el dominio y comprobar que la baja funciona.

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

Rebotes, bajas y analítica

La API del proveedor solo confirma que aceptó la petición. No demuestra lectura, clic ni interés. Normaliza los webhooks a un modelo propio y actualiza una suppression list cuando haya rebote duro, queja o baja.

// 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(),
  });
}

No te quedes solo con aperturas. Bloqueo de imágenes y protección de privacidad distorsionan ese dato. Mide descargas, clics de CTA, formularios de consultoría iniciados, respuestas, bajas, rebotes y compras. Usa nombres como lead_magnet_requested, email_cta_click y consultation_request_started.

Casos de uso reales

Primer caso: PDF gratuito debajo de un artículo técnico. Envía la descarga al instante, luego una explicación del fallo más común, después una plantilla de producto y finalmente una invitación a formación o consultoría. Un email, una acción principal, enlace de baja siempre visible.

Segundo caso: onboarding tras compra. Una persona que compró una guía o entró en un taller necesita empezar, resolver bloqueos y descubrir material avanzado. La mejor venta adicional suele ser ayudarle a tener éxito, no llenar el recibo de promociones.

Tercer caso: seguimiento de consultoría. Incluye notas de la reunión, decisiones, próximos pasos, enlaces relevantes, plazo de respuesta y CTA de reserva o propuesta. Si el mensaje ignora la conversación real, parecerá spam.

Cuarto caso: reactivación de leads inactivos. Envía solo actualizaciones importantes, historias de fallos útiles o recursos nuevos. Si no vuelven clics ni respuestas, reduce frecuencia o detén la secuencia.

Fallos frecuentes

El primer fallo es exponer la API key en el navegador. El envío debe vivir en el servidor.

El segundo es enviar desde un dominio no autenticado. Configura SPF, DKIM y DMARC para tu dominio.

El tercero es ignorar bajas y rebotes. Unsubscribe, complaint y hard bounce deben salir de las campañas normales.

El cuarto es reintentar inmediatamente tras un rate limit. Maneja 429 y 5xx temporales con backoff y ritmo estable. Los límites cambian por cuenta, plan, reputación y destinatario.

El quinto es mezclar emails transaccionales y promocionales. Reset de contraseña, recibos y alertas deben ser claros. Los CTA comerciales pertenecen a mensajes con consentimiento y contexto.

CTA de monetización

El sistema está completo cuando el lector puede elegir el siguiente paso sin presión. En ClaudeCodeLab, principiantes pueden empezar con el PDF gratuito, quienes implementan pueden revisar productos y plantillas, y equipos que necesitan aplicarlo a un repositorio real pueden usar formación y consultoría.

Al probar este flujo, la mejora más grande vino de diseñar consentimiento, bajas, rebotes y analítica de CTA antes del código específico del proveedor. Empieza con un solo email de lead magnet, verifica entrega, baja, rebote y clic, y luego amplía a onboarding y seguimiento.

#Claude Code #email automation #lead capture #Resend #SendGrid #Node.js
Gratis

PDF gratis: cheatsheet de Claude Code

Introduce tu email y descarga una hoja con comandos, hábitos de revisión y flujos seguros.

Cuidamos tus datos y no enviamos spam.

Masa

Sobre el autor

Masa

Ingeniero enfocado en workflows prácticos con Claude Code.