Use Cases (Atualizado: 02/06/2026)

Como implementar e-mail SendGrid com Claude Code em segurança

Implemente SendGrid com Claude Code: remetente verificado, Mail Send API, retries, logs e entregabilidade.

Como implementar e-mail SendGrid com Claude Code em segurança

SendGrid é um serviço em nuvem para enviar e-mails de aplicação por API. Ele serve para confirmações de formulário de contato, onboarding após cadastro, relatórios diários, notificações transacionais e follow-up comercial quando há contexto legítimo e uma opção clara de opt-out.

O problema é que o código parece simples demais. Se você pedir ao Claude Code apenas “envie e-mail com SendGrid”, provavelmente receberá uma chamada API que funciona, mas pode faltar remetente verificado, proteção da API key, prevenção de envio duplicado em retry, tratamento de bounce, reclamação de spam, logs do provedor e regras de cancelamento. E-mail enviado não volta. Por isso, a fronteira operacional deve vir antes do fetch.

Este guia usa como base a documentação oficial da Twilio SendGrid para a v3 Mail Send API, a página de erros de validação e o site do SendGrid. Você terá um script Node.js copiável e seguro por padrão: dry-run sem --send, validação de payload, modo sandbox, retry apenas para falhas temporárias e log local como exemplo de guarda de idempotência.

Para completar o contexto, veja também automação de e-mail com Claude Code, desenvolvimento de API, gestão de variáveis de ambiente e boas práticas de segurança.

Fundamentos do SendGrid antes do código

A API Mail Send recebe JSON em POST https://api.sendgrid.com/v3/mail/send com o cabeçalho Authorization: Bearer SENDGRID_API_KEY. A chamada é simples, mas produção exige preparação.

ItemSignificado práticoO que verificar
Remetente verificadoSendGrid confirma que o endereço from pode enviarSingle Sender em testes, Domain Authentication em produção
Autenticação de domínioDNS prova que seu domínio pode enviar via SendGridSPF/DKIM verificados antes do tráfego real
API keySegredo usado pelo servidor para chamar o SendGridSomente no servidor, nunca no navegador ou no Git
personalizationsDados por destinatário: to, assunto, custom args ou dados de templateUm destinatário por personalization para não expor listas
SuppressionEndereços bloqueados por bounce, reclamação ou descadastroConsultar sua própria lista antes de chamar o SendGrid
Log do provedorStatus HTTP, corpo da resposta e x-message-idGuardar dados suficientes para suporte e anti-duplicação

SPF é um registro DNS que informa quais servidores podem enviar por seu domínio. DKIM assina a mensagem para que o destinatário verifique que ela foi autorizada e não alterada. DMARC define a política quando SPF ou DKIM não alinham. Para começar, basta a ideia principal: autenticação de remetente é o documento de identidade da entregabilidade.

Não coloque um Gmail aleatório no from. Para prova local, use Single Sender verificado. Em produção, autentique o domínio e envie por um endereço real de produto, suporte ou equipe. Muitos erros de validação surgem de from inválido, personalizations incorretas, conteúdo ausente ou template mal configurado.

Quatro casos de uso práticos

Não esconda todos os e-mails em uma função genérica sendMail. Cada fluxo tem consentimento, frequência, tom, risco e log diferentes.

Caso de usoExemploProteção necessária
Formulário de contatoConfirmação ao visitante e aviso à equipeEscapar entrada do usuário, separar e-mail interno e externo
Onboarding transacionalCadastro concluído, primeiro acesso, instruções de compraManter o conteúdo esperado e útil, sem marketing agressivo
Relatório diárioVendas, erros, reservas, progresso de cursoUsar idempotency key para retry não parecer duplicado
Vendas ou outreachFollow-up de reunião, proposta, material prometidoIncluir opt-out, identidade do remetente e suppression

Outreach precisa de cuidado extra. Conseguir enviar tecnicamente não significa que o envio é apropriado. Regras variam por país, relação prévia, B2B/B2C e tipo de mensagem. Este artigo é guia de implementação, não aconselhamento jurídico. No mínimo, explique o motivo do contato, identifique o remetente e forneça uma forma funcional de opt-out.

flowchart LR
  App["App / mudança do Claude Code"]
  Validate["Validação do payload"]
  Log["Log e idempotency key"]
  SendGrid["SendGrid Mail Send API"]
  Inbox["Caixa de entrada"]
  Events["Bounce / Spam / Unsubscribe"]
  Suppression["Lista de suppression"]

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

Script Node.js copiável

O script abaixo roda em Node.js 20 ou superior e não usa dependências. Por padrão ele faz dry-run: imprime o payload, grava o log e não chama o SendGrid. Use --send para chamada real e --send --sandbox para o SendGrid validar a requisição sem entregar o e-mail.

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

Comece pelo dry-run. No 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

No macOS ou Linux:

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

O log JSON local é propositalmente simples. Em produção, leve a mesma ideia para Postgres, Redis, SQS, Cloud Tasks ou outra fila durável. Coloque restrição única em idempotency_key e separe status do provedor do status de negócio.

Prompt para Claude Code

Um bom prompt pede fronteiras operacionais, não só código.

Adicione envio de e-mail com SendGrid a este repositório.
Os fluxos são confirmação de formulário de contato, onboarding de cadastro, relatórios diários e follow-up comercial.

Restrições:
- Usar SendGrid Mail Send API v3.
- Ler a API key apenas da variável de ambiente do servidor SENDGRID_API_KEY.
- Todos os scripts devem ser dry-run por padrão e enviar somente com --send.
- Usar exatamente um destinatário por personalization para não expor listas.
- Repetir apenas 429 e 5xx com exponential backoff.
- Verificar unsubscribe, bounce e spam complaint antes do envio.
- Salvar provider response, HTTP status, x-message-id e idempotency key.
- E-mails de outreach precisam ter caminho de opt-out.
- Linkar a documentação oficial do SendGrid no README.

Primeiro devolva a tabela de design e a lista de arquivos. Aguarde aprovação antes de editar.

Assim o Claude Code precisa considerar consentimento, suppression, logs, retries e escopo de arquivos. Isso também reduz conflito quando outras pessoas trabalham no mesmo repositório.

Falhas comuns

FalhaConsequênciaPrevenção
Vazamento da API keyTerceiros podem enviar pela sua conta e prejudicar reputaçãoIgnorar .env, escanear secrets e rotacionar imediatamente
Remetente não verificadoErros 400, bloqueios ou pior inbox placementVerificar Single Sender ou autenticar domínio
Retry duplicadoO mesmo relatório, recibo ou follow-up chega mais de uma vezUsar send log e idempotency key antes do provider
Outreach sem opt-outAumentam reclamações e risco legalIncluir descadastro, identidade e motivo do envio
Enviar rápido demaisRate limits e problemas de reputaçãoComeçar com pouco volume e observar bounces e complaints
Não salvar resposta do provedorSuporte não consegue explicar o incidenteGuardar status, body, x-message-id e hash do destinatário
Expor lista de destinatáriosUsuários veem e-mails de outras pessoasUm destinatário por personalization

Um 202 Accepted do SendGrid não prova que a mensagem chegou à caixa de entrada. Ele diz apenas que o SendGrid aceitou a requisição para processamento. Para operação real, acompanhe bounce, block, spam report e unsubscribe depois do envio.

Entregabilidade e CTA

Entregabilidade não depende só de DNS. Expectativa do destinatário, frequência, clareza do conteúdo, histórico de bounce, taxa de reclamação e facilidade de descadastro também contam. No mínimo, monitore enviados, accepted, bounces, blocked, spam reports e unsubscribes.

Em um funil no estilo ClaudeCodeLab, o CTA deve respeitar o contexto. Uma confirmação de contato pode apontar para um artigo útil. Onboarding pode oferecer checklist ou template. Relatório diário deve continuar operacional. Follow-up comercial só deve convidar para conversa quando a relação justificar. Para aplicar em um repositório real, a página de treinamento e consultoria Claude Code pode cobrir SendGrid, variáveis de ambiente, revisão de segurança, suppression e logs.

Resultado da verificação prática

Quando Masa testou este exemplo, a escolha mais útil foi tornar dry-run o padrão. Sem flags, o script apenas imprimiu o payload e gravou o log local. Com --send e MAIL_FROM em @example.com, ele parou antes da chamada API. Com --send --sandbox, o SendGrid validou o formato da requisição sem entregar e-mail. Em projetos reais, esse log local deve virar uma fila em banco com restrição única de idempotência e checks de bounce, spam complaint e unsubscribe antes de cada envio.

#Claude Code #SendGrid #email #API #automation
Grátis

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.

Masa

Sobre o autor

Masa

Engenheiro focado em workflows práticos com Claude Code.