Use Cases (Actualizado: 2/6/2026)

Cómo implementar correos SendGrid con Claude Code de forma segura

Implementa SendGrid con Claude Code: remitente verificado, Mail Send API, reintentos, logs y entregabilidad.

Cómo implementar correos SendGrid con Claude Code de forma segura

SendGrid es un servicio de entrega de correo en la nube para enviar emails de aplicación mediante API. Sirve para confirmaciones de formularios de contacto, onboarding después de un registro, reportes diarios, avisos transaccionales y seguimiento comercial cuando existe una base legítima y una salida clara para darse de baja.

El riesgo es que el código parece demasiado sencillo. Si le pides a Claude Code “envía email con SendGrid”, probablemente obtendrás una llamada API que funciona, pero no necesariamente un remitente verificado, manejo correcto del API key, protección contra duplicados en reintentos, gestión de bounces, quejas de spam, logs del proveedor ni opt-out. El correo sale de tu sistema y no se puede retirar después. Por eso conviene diseñar la frontera operativa antes de escribir el fetch.

Esta guía usa como base la documentación oficial de Twilio SendGrid para la v3 Mail Send API, la referencia de errores de validación y el sitio de SendGrid. Incluye un script Node.js copiable que es seguro por defecto: ejecuta dry-run si no pasas --send, valida el payload, permite validación en sandbox, reintenta solo fallos temporales y guarda un log local como demostración de idempotencia.

Para completar la base, enlaza este tema con automatización de email con Claude Code, desarrollo de API, gestión de variables de entorno y buenas prácticas de seguridad.

Conceptos de SendGrid antes del código

La API Mail Send es directa: POST https://api.sendgrid.com/v3/mail/send con JSON y el encabezado Authorization: Bearer SENDGRID_API_KEY. Lo que decide la calidad no es solo esa llamada, sino todo lo que la rodea.

ElementoSignificado prácticoQué verificar
Remitente verificadoSendGrid confirma que puedes enviar desde ese fromSingle Sender para pruebas pequeñas, Domain Authentication para producción
Autenticación de dominioDNS demuestra que tu dominio puede enviar mediante SendGridSPF/DKIM verificados antes de enviar tráfico real
API keySecreto que permite al servidor llamar a SendGridSolo en variables de entorno del servidor, nunca en frontend ni Git
personalizationsDatos por destinatario: to, asunto, custom args o datos de plantillaUn destinatario por personalization para no exponer listas
SuppressionDirecciones bloqueadas por bounce, queja o bajaConsultar tu propia lista antes de enviar
Log del proveedorEstado HTTP, cuerpo de respuesta y x-message-idGuardar lo necesario para soporte, auditoría y evitar duplicados

SPF es un registro DNS que indica qué servidores pueden enviar por tu dominio. DKIM firma el mensaje para que el receptor pueda verificar que fue autorizado y no se modificó. DMARC indica qué política aplicar cuando SPF o DKIM no alinean. No hace falta memorizar todos los detalles al principio, pero sí entender que la autenticación del remitente es la base de la entregabilidad.

No empieces con una dirección Gmail cualquiera en from. Para una prueba local, verifica un Single Sender. Para producción, autentica tu dominio y usa una dirección real de producto, soporte o equipo. Muchos errores de validación vienen de from inválido, personalizations mal formado, falta de contenido o uso incorrecto de plantillas.

Cuatro casos de uso reales

No trates todos los correos como una sola función sendMail. Cada flujo tiene consentimiento, frecuencia, tono, riesgo y logging diferentes.

Caso de usoEjemploControl necesario
Formulario de contactoConfirmación al visitante y aviso al equipoEscapar entradas del usuario, separar correo interno y correo al visitante
Onboarding transaccionalRegistro completado, guía de primer acceso, instrucciones de compraMantenerlo esperado y útil, sin convertirlo en publicidad agresiva
Reporte diarioVentas, errores, reservas, progreso de cursoUsar idempotency key para que un reintento no parezca reporte duplicado
Ventas u outreachSeguimiento después de una reunión, propuesta, recurso prometidoIncluir opt-out, respetar suppressions y revisar requisitos legales locales

En ventas y outreach hay que ser especialmente cuidadoso. Poder enviar técnicamente no significa que debas hacerlo. Las reglas dependen del país, la relación previa, si es B2B o B2C y el tipo de mensaje. Este artículo no sustituye asesoría legal. Como mínimo, explica por qué escribes, identifica al remitente y ofrece una forma funcional de darse de baja.

flowchart LR
  App["App / cambio de Claude Code"]
  Validate["Validación del payload"]
  Log["Log e idempotency key"]
  SendGrid["SendGrid Mail Send API"]
  Inbox["Bandeja de entrada"]
  Events["Bounce / Spam / Unsubscribe"]
  Suppression["Lista de suppression"]

  App --> Validate --> Log --> SendGrid --> Inbox
  SendGrid --> Events --> Suppression
  Suppression --> Validate

Script Node.js copiable

El siguiente script funciona con Node.js 20 o superior y no necesita dependencias. Por defecto hace dry-run: imprime el payload y guarda el log, pero no llama a SendGrid. Usa --send para una llamada real y --send --sandbox para que SendGrid valide la solicitud sin entregar el correo.

// sendgrid-safe-send.mjs
import { createHash } from "node:crypto";
import { existsSync } from "node:fs";
import { readFile, writeFile } from "node:fs/promises";

const ENDPOINT = process.env.SENDGRID_API_BASE ?? "https://api.sendgrid.com/v3/mail/send";
const LOG_PATH = process.env.SENDGRID_SEND_LOG ?? ".sendgrid-send-log.json";
const DRY_RUN = !process.argv.includes("--send");
const SANDBOX = process.argv.includes("--sandbox");
const MAX_ATTEMPTS = Number.parseInt(process.env.SENDGRID_MAX_ATTEMPTS ?? "3", 10);

const recipient = {
  email: process.env.MAIL_TO ?? "recipient@example.com",
  name: process.env.MAIL_TO_NAME ?? "Test Recipient",
};

const message = {
  from: {
    email: process.env.MAIL_FROM ?? "verified-sender@example.com",
    name: process.env.MAIL_FROM_NAME ?? "ClaudeCodeLab Demo",
  },
  reply_to: {
    email: process.env.MAIL_REPLY_TO ?? process.env.MAIL_FROM ?? "verified-sender@example.com",
  },
  personalizations: [
    {
      to: [recipient],
      custom_args: {
        use_case: process.env.MAIL_USE_CASE ?? "dry_run_demo",
      },
    },
  ],
  subject: process.env.MAIL_SUBJECT ?? `SendGrid dry-run test for ${recipient.name}`,
  content: [
    {
      type: "text/plain",
      value: `Hello ${recipient.name},\n\nThis is a safe SendGrid test from Claude Code.\n`,
    },
    {
      type: "text/html",
      value: `<p>Hello ${escapeHtml(recipient.name)},</p><p>This is a safe SendGrid test from Claude Code.</p>`,
    },
  ],
  categories: ["claude-code-demo"],
  mail_settings: {
    sandbox_mode: { enable: SANDBOX },
  },
};

validatePayload(message);
const idempotencyKey = makeIdempotencyKey(message);
for (const personalization of message.personalizations) {
  personalization.custom_args = {
    ...(personalization.custom_args ?? {}),
    idempotency_key: idempotencyKey,
  };
}

await sendWithRetry(message, idempotencyKey);

function validatePayload(payload) {
  if (!Number.isInteger(MAX_ATTEMPTS) || MAX_ATTEMPTS < 1 || MAX_ATTEMPTS > 5) {
    throw new Error("SENDGRID_MAX_ATTEMPTS must be an integer from 1 to 5.");
  }

  assertEmail(payload.from?.email, "from.email");
  if (!DRY_RUN && payload.from.email.endsWith("@example.com")) {
    throw new Error("Set MAIL_FROM to a verified SendGrid sender before using --send.");
  }

  if (!Array.isArray(payload.personalizations) || payload.personalizations.length === 0) {
    throw new Error("personalizations must contain at least one recipient.");
  }

  for (const [index, personalization] of payload.personalizations.entries()) {
    if (!Array.isArray(personalization.to) || personalization.to.length !== 1) {
      throw new Error(`personalizations[${index}].to must contain exactly one recipient.`);
    }
    assertEmail(personalization.to[0]?.email, `personalizations[${index}].to[0].email`);
  }

  if (!payload.subject && !payload.template_id) {
    throw new Error("Provide a subject or a SendGrid template_id.");
  }

  const hasContent = Array.isArray(payload.content)
    && payload.content.some((item) => typeof item.value === "string" && item.value.trim());
  if (!hasContent && !payload.template_id) {
    throw new Error("Provide text/html content or a SendGrid template_id.");
  }
}

function assertEmail(value, field) {
  if (typeof value !== "string" || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) {
    throw new Error(`${field} must be a valid email address.`);
  }
}

function makeIdempotencyKey(payload) {
  const stableEnvelope = {
    from: payload.from.email.toLowerCase(),
    to: payload.personalizations.map((item) => item.to[0].email.toLowerCase()),
    subject: payload.subject,
    content: payload.content?.map((item) => item.value),
    useCase: payload.personalizations.map((item) => item.custom_args?.use_case ?? ""),
  };
  return createHash("sha256").update(JSON.stringify(stableEnvelope)).digest("hex").slice(0, 32);
}

async function sendWithRetry(payload, idempotencyKey) {
  const log = await readJsonLog();
  const previous = log[idempotencyKey];

  if (previous?.status === "accepted") {
    console.log(`Already accepted by SendGrid. idempotencyKey=${idempotencyKey}`);
    return;
  }
  if (previous?.status === "pending") {
    throw new Error(`A send is already pending. idempotencyKey=${idempotencyKey}`);
  }

  if (DRY_RUN) {
    log[idempotencyKey] = {
      status: "dry-run",
      updatedAt: new Date().toISOString(),
      to: payload.personalizations.map((item) => item.to[0].email),
    };
    await writeJsonLog(log);
    console.log("Dry run only. Add --send to call SendGrid.");
    console.log(JSON.stringify({ idempotencyKey, payload }, null, 2));
    return;
  }

  const apiKey = process.env.SENDGRID_API_KEY;
  if (!apiKey) {
    throw new Error("SENDGRID_API_KEY is required when using --send.");
  }

  log[idempotencyKey] = { status: "pending", updatedAt: new Date().toISOString() };
  await writeJsonLog(log);

  for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt += 1) {
    const response = await fetch(ENDPOINT, {
      method: "POST",
      headers: {
        Authorization: `Bearer ${apiKey}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify(payload),
    });
    const responseBody = await response.text();
    const providerMessageId = response.headers.get("x-message-id");

    if (response.status === 202) {
      log[idempotencyKey] = {
        status: "accepted",
        statusCode: response.status,
        providerMessageId,
        updatedAt: new Date().toISOString(),
      };
      await writeJsonLog(log);
      console.log(`Accepted by SendGrid. idempotencyKey=${idempotencyKey}`);
      return;
    }

    const retryable = response.status === 429 || response.status >= 500;
    log[idempotencyKey] = {
      status: retryable && attempt < MAX_ATTEMPTS ? "retrying" : "failed",
      statusCode: response.status,
      responseBody: responseBody.slice(0, 2000),
      attempt,
      updatedAt: new Date().toISOString(),
    };
    await writeJsonLog(log);

    if (!retryable || attempt === MAX_ATTEMPTS) {
      throw new Error(`SendGrid request failed with HTTP ${response.status}: ${responseBody}`);
    }

    await sleep(Math.min(1000 * 2 ** (attempt - 1), 8000));
  }
}

async function readJsonLog() {
  if (!existsSync(LOG_PATH)) return {};
  return JSON.parse(await readFile(LOG_PATH, "utf8"));
}

async function writeJsonLog(log) {
  await writeFile(LOG_PATH, `${JSON.stringify(log, null, 2)}\n`, "utf8");
}

function sleep(ms) {
  return new Promise((resolve) => setTimeout(resolve, ms));
}

function escapeHtml(value) {
  return String(value)
    .replaceAll("&", "&amp;")
    .replaceAll("<", "&lt;")
    .replaceAll(">", "&gt;")
    .replaceAll('"', "&quot;")
    .replaceAll("'", "&#39;");
}

Primero ejecuta dry-run. En Windows PowerShell:

node .\sendgrid-safe-send.mjs

$env:SENDGRID_API_KEY="SG.xxxxx"
$env:MAIL_FROM="verified@example.com"
$env:MAIL_TO="you@example.net"
node .\sendgrid-safe-send.mjs --send --sandbox

node .\sendgrid-safe-send.mjs --send

En macOS o Linux:

SENDGRID_API_KEY="SG.xxxxx" MAIL_FROM="verified@example.com" MAIL_TO="you@example.net" node sendgrid-safe-send.mjs --send --sandbox

El log JSON local es solo para aprendizaje. En producción, mueve la misma idea a Postgres, Redis, SQS, Cloud Tasks u otra cola duradera. Añade una restricción única para idempotency_key y guarda el estado del proveedor separado del estado de negocio.

Prompt para Claude Code

Un prompt útil no pide solo código. Pide diseño, límites y verificación.

Agrega envío de email con SendGrid a este repositorio.
Los flujos son confirmación de formulario de contacto, onboarding de registro, reportes diarios y seguimiento comercial.

Restricciones:
- Usar SendGrid Mail Send API v3.
- Leer el API key solo desde la variable de entorno del servidor SENDGRID_API_KEY.
- Todos los scripts deben ser dry-run por defecto y enviar solo con --send.
- Usar exactamente un destinatario por personalization para no exponer listas.
- Reintentar solo 429 y 5xx con exponential backoff.
- Revisar unsubscribe, bounce y spam complaint antes de enviar.
- Guardar provider response, HTTP status, x-message-id e idempotency key.
- Incluir ruta de opt-out en emails de outreach.
- Dejar enlaces a la documentación oficial de SendGrid en el README.

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

Así obligas a Claude Code a pensar en consentimiento, suppression, logs y reintentos. También reduces conflictos en un repositorio con varias personas, porque primero aparece el alcance de archivos.

Fallos comunes

FalloConsecuenciaPrevención
Filtrar el API keyTerceros pueden enviar desde tu cuenta y dañar reputaciónIgnorar .env, escanear secretos y rotar la clave
Remitente no verificadoErrores 400, bloqueos o mala ubicación en inboxVerificar Single Sender o autenticar dominio
Reintento duplicadoLlega dos veces el reporte, recibo o correo comercialUsar send log e idempotency key antes de llamar al proveedor
Outreach sin opt-outAumentan quejas y riesgo legalIncluir baja, identidad de empresa y motivo del mensaje
Enviar demasiado rápidoRate limits y problemas de reputaciónEmpezar con poco volumen y observar bounces y quejas
No guardar respuesta del proveedorSoporte no puede explicar qué pasóGuardar status, body, x-message-id y hash del destinatario
Exponer lista de destinatariosUn usuario ve emails de otros usuariosUn destinatario por personalization

Un 202 Accepted de SendGrid no significa que el mensaje llegó a la bandeja de entrada. Significa que SendGrid aceptó la solicitud para procesarla. Para operar bien necesitas eventos posteriores: bounces, blocks, spam reports y unsubscribes.

Entregabilidad y CTA

La entregabilidad no depende solo del DNS. También importan la expectativa del destinatario, el volumen, la claridad del contenido, la tasa de bounce, las quejas y la facilidad para darse de baja. Como mínimo mide enviados, accepted, bounces, blocked, spam reports y unsubscribes.

En un embudo de ClaudeCodeLab, el CTA debe respetar el momento del lector. Una confirmación de contacto puede enlazar a un artículo útil; un onboarding puede ofrecer una checklist o plantilla; un reporte diario debe seguir siendo operativo; un seguimiento comercial solo debe invitar a consultoría si la relación lo justifica. Para implementarlo en un repositorio real, la página de formación y consultoría Claude Code puede cubrir variables de entorno, SendGrid, revisión de seguridad, suppression y diseño de logs.

Resultado de la verificación práctica

Cuando Masa probó este ejemplo, la decisión más útil fue hacer que dry-run fuera el valor por defecto. Sin flags, el script solo imprimió el payload y escribió el log local. Con --send y un MAIL_FROM de @example.com, se detuvo antes de llamar a la API. Con --send --sandbox, SendGrid pudo validar la forma de la solicitud sin entregar correo. En proyectos reales, ese log local debe convertirse en una cola con base de datos, restricción única de idempotencia y checks de bounce, spam complaint y unsubscribe antes de cada envío.

#Claude Code #SendGrid #email #API #automation
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.