Use Cases (Mis à jour: 02/06/2026)

Implémenter SendGrid avec Claude Code sans négliger la sécurité

Implémentez SendGrid avec Claude Code: expéditeur vérifié, Mail Send API, retries, logs et délivrabilité.

Implémenter SendGrid avec Claude Code sans négliger la sécurité

SendGrid est un service cloud d’envoi d’e-mails applicatifs par API. Il peut servir aux confirmations de formulaire de contact, aux e-mails d’onboarding, aux rapports quotidiens, aux notifications transactionnelles et aux relances commerciales lorsque le cadre de consentement et de désinscription est clair.

Le piège est que le code paraît simple. Si vous demandez seulement à Claude Code “ajoute l’envoi SendGrid”, vous obtiendrez probablement un appel API fonctionnel, mais pas forcément la vérification de l’expéditeur, la protection de la clé API, la prévention des doublons lors des retries, la gestion des bounces, les plaintes spam, les logs fournisseur ni l’opt-out. Un e-mail envoyé ne se récupère pas. Il faut donc définir la frontière opérationnelle avant la fonction fetch.

Ce guide s’appuie sur la documentation officielle Twilio SendGrid de la v3 Mail Send API, la page SendGrid des erreurs de validation et le site SendGrid. Vous y trouverez un script Node.js prêt à copier, sûr par défaut: dry-run sans --send, validation du payload, mode sandbox, retries limités aux erreurs temporaires et log local pour illustrer l’idempotence.

Pour les fondations voisines, consultez aussi l’automatisation e-mail avec Claude Code, le développement d’API, la gestion des variables d’environnement et les bonnes pratiques de sécurité.

Les bases SendGrid avant le code

L’API Mail Send consiste à envoyer du JSON vers POST https://api.sendgrid.com/v3/mail/send avec l’en-tête Authorization: Bearer SENDGRID_API_KEY. Cette partie est simple. Ce qui rend l’implémentation fiable, c’est la préparation autour.

ÉlémentSens pratiqueÀ vérifier
Expéditeur vérifiéSendGrid confirme que l’adresse from peut envoyerSingle Sender pour un test, Domain Authentication pour la production
Authentification de domaineLe DNS prouve que votre domaine peut envoyer via SendGridSPF/DKIM validés avant le trafic réel
Clé APISecret utilisé par le serveur pour appeler SendGridUniquement côté serveur, jamais dans le navigateur ni dans Git
personalizationsDonnées par destinataire: to, sujet, custom args, données de templateUn destinataire par personalization pour ne pas exposer une liste
SuppressionAdresses à exclure après bounce, plainte ou désinscriptionVérifier votre propre liste avant l’appel SendGrid
Log fournisseurStatut HTTP, corps de réponse et x-message-idGarder assez d’informations pour support et anti-doublon

SPF est un enregistrement DNS qui indique quels serveurs peuvent envoyer pour votre domaine. DKIM signe le message pour prouver qu’il est autorisé et non modifié. DMARC définit la politique côté réception lorsque SPF ou DKIM échoue. Pour un débutant, l’idée essentielle est simple: l’authentification de l’expéditeur est la pièce d’identité de la délivrabilité.

N’utilisez pas une adresse Gmail au hasard dans from. Pour une preuve de concept, vérifiez un Single Sender. En production, authentifiez votre domaine et envoyez depuis une adresse produit, support ou équipe. Beaucoup d’erreurs de validation viennent d’un from invalide, de personalizations mal formées, d’un contenu absent ou d’une mauvaise configuration de template.

Quatre cas d’usage à distinguer

Ne mettez pas tous les e-mails derrière une seule fonction sendMail générique. Chaque flux a son consentement, sa fréquence, son ton, ses risques et ses logs.

Cas d’usageExempleGarde-fou nécessaire
Formulaire de contactConfirmation au visiteur et notification interneÉchapper les champs utilisateur, séparer mail admin et mail visiteur
Onboarding transactionnelInscription confirmée, guide de première connexion, achatRester attendu et utile, sans transformer l’e-mail en promotion forcée
Rapport quotidienChiffre d’affaires, erreurs, réservations, progressionUtiliser une idempotency key pour éviter les doublons lors d’un retry
Vente ou outreachSuivi après rendez-vous, proposition, ressource promiseAjouter opt-out, identité de l’expéditeur et vérification suppression

L’outreach doit être traité avec prudence. Pouvoir envoyer techniquement ne signifie pas que l’envoi est approprié. Les règles changent selon le pays, la relation, le contexte B2B ou B2C et le contenu du message. Cet article n’est pas un avis juridique. Au minimum, donnez la raison du contact, identifiez l’expéditeur et fournissez une désinscription fonctionnelle.

flowchart LR
  App["App / changement Claude Code"]
  Validate["Validation du payload"]
  Log["Log et idempotency key"]
  SendGrid["SendGrid Mail Send API"]
  Inbox["Boîte de réception"]
  Events["Bounce / Spam / Unsubscribe"]
  Suppression["Liste de suppression"]

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

Script Node.js prêt à copier

Le script suivant fonctionne avec Node.js 20 ou plus, sans dépendance. Par défaut, il fait un dry-run: il affiche le payload, écrit le log et ne contacte pas SendGrid. Utilisez --send pour un vrai appel API, et --send --sandbox pour faire valider la requête par SendGrid sans livrer l’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;");
}

Commencez par le dry-run. Sous 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

Sous 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

Le log JSON local est volontairement simple. En production, utilisez Postgres, Redis, SQS, Cloud Tasks ou une autre file durable. Ajoutez une contrainte unique sur idempotency_key et séparez l’état fournisseur de l’état métier.

Prompt utile pour Claude Code

Un prompt vague produit souvent seulement une fonction d’envoi. Donnez à Claude Code le but métier, les limites de sécurité et les conditions de vérification.

Ajoute l'envoi d'e-mails SendGrid dans ce dépôt.
Les flux sont: confirmation de formulaire de contact, onboarding après inscription, rapports quotidiens et suivi commercial.

Contraintes:
- Utiliser SendGrid Mail Send API v3.
- Lire la clé API uniquement depuis la variable serveur SENDGRID_API_KEY.
- Tous les scripts sont en dry-run par défaut et envoient seulement avec --send.
- Utiliser exactement un destinataire par personalization pour ne pas exposer les listes.
- Réessayer seulement 429 et 5xx avec exponential backoff.
- Vérifier unsubscribe, bounce et spam complaint avant l'envoi.
- Stocker provider response, HTTP status, x-message-id et idempotency key.
- Ajouter un chemin opt-out pour les e-mails d'outreach.
- Lier la documentation officielle SendGrid dans le README.

Retourne d'abord la table de conception et la liste des fichiers. Attends mon accord avant modification.

Ce cadrage oblige Claude Code à traiter consentement, suppression, logs et retries. Il limite aussi les conflits lorsque plusieurs personnes modifient le dépôt.

Échecs fréquents à anticiper

ÉchecConséquencePrévention
Fuite de clé APIQuelqu’un peut envoyer depuis votre compte et nuire à la réputationIgnorer .env, scanner les secrets, faire une rotation immédiate
Expéditeur non vérifiéErreurs 400, blocages ou mauvaise délivrabilitéVérifier Single Sender ou authentifier le domaine
Retry qui dupliqueLe même reçu, rapport ou message commercial arrive plusieurs foisUtiliser send log et idempotency key avant l’appel fournisseur
Outreach sans opt-outPlaintes et risque juridique augmententInclure désinscription, identité et raison de l’envoi
Volume trop rapideRate limits et dégradation de réputationCommencer petit et surveiller bounces et plaintes
Pas de réponse fournisseur stockéeLe support ne peut pas expliquer l’incidentSauver status, body, x-message-id et hash destinataire
Liste de destinataires exposéeLes adresses d’autres utilisateurs deviennent visiblesUn destinataire par personalization

Un 202 Accepted de SendGrid ne prouve pas que l’e-mail est arrivé en boîte de réception. Il indique que SendGrid a accepté la demande. Les événements de bounce, block, spam report et unsubscribe restent nécessaires pour piloter la délivrabilité.

Délivrabilité et CTA

La délivrabilité ne dépend pas seulement du DNS. Elle dépend aussi des attentes du destinataire, de la fréquence, du contenu, de l’historique de bounce, du taux de plainte et de la facilité de désinscription. Suivez au minimum les volumes envoyés, accepted, bounces, blocked, spam reports et unsubscribes.

Dans un funnel ClaudeCodeLab, le CTA doit correspondre au contexte. Une confirmation de contact peut renvoyer vers un article utile. Un onboarding peut proposer une checklist ou un template. Un rapport quotidien doit rester opérationnel. Un suivi commercial ne devrait inviter à une consultation que si la relation le justifie. Pour une mise en place dans un vrai dépôt, la page formation et conseil Claude Code peut couvrir variables d’environnement, SendGrid, revue de sécurité, suppression et logs.

Résultat de vérification pratique

Quand Masa a testé cet exemple, le choix le plus utile a été le dry-run par défaut. Sans option, le script affiche le payload et écrit le log local. Avec --send et un MAIL_FROM en @example.com, il s’arrête avant l’appel API. Avec --send --sandbox, SendGrid peut valider la forme de la requête sans livrer l’e-mail. En projet réel, ce log local doit devenir une file en base avec contrainte unique d’idempotence, alimentée par les événements bounce, spam complaint et unsubscribe avant chaque envoi.

#Claude Code #SendGrid #email #API #automation
Gratuit

PDF gratuit: cheatsheet Claude Code

Saisissez votre email et téléchargez une page avec commandes, habitudes de review et workflow sûr.

Nous protégeons vos données et n'envoyons pas de spam.

Masa

À propos de l'auteur

Masa

Ingénieur spécialisé dans les workflows pratiques avec Claude Code.