Automatisation email avec Claude Code : du lead à la monétisation
Créez une automatisation email avec Claude Code : lead magnet, consentement, retry, analytics et ventes.
L’automatisation email ne se limite pas à envoyer un message après un formulaire. Un système utile pour la monétisation livre le lead magnet, démarre une séquence d’onboarding, relance une demande de consultation, conserve le consentement, traite les désabonnements, supprime les adresses en hard bounce, réessaie les erreurs temporaires et mesure quels CTA mènent à un produit, une formation ou une mission de conseil.
Claude Code est pertinent ici parce que l’email traverse plusieurs couches à la fois : schémas, templates, adaptateurs de fournisseur, jobs en arrière-plan, webhooks, analytics et documentation. Lors de la refonte du tunnel PDF gratuit de ce site, l’erreur initiale a été de générer seulement une fonction d’envoi Resend. Elle marchait, mais le consentement, l’URL de désabonnement, les bounces et les clics CTA ont été ajoutés après coup. La meilleure méthode consiste à demander d’abord un plan borné, puis à autoriser Claude Code à modifier uniquement les fichiers prévus.
Ce guide propose une base Node.js/TypeScript indépendante du fournisseur, compatible avec une API de style Resend ou SendGrid. Nous couvrons lead magnet, onboarding, suivi de consultation, bases SPF/DKIM/DMARC, limites d’envoi, queue/retry, templates, analytics et CTA vers produits, formation ou consulting. Pour le chemin de revenus, consultez aussi l’audit du funnel de contenu, l’implémentation analytics et la gestion cookies/consentement.
Concevoir avant de coder
Un lead magnet est une ressource gratuite, par exemple un PDF, une checklist ou un template, proposée contre une adresse email. L’onboarding est une suite de messages qui aide une personne à démarrer après inscription ou achat. Le suivi de consultation est un email opérationnel avec notes, prochaines étapes, proposition ou lien de réservation.
Ne mettez pas tout dans une newsletter générique. Chaque type de message a son consentement, ses métriques, son ton et ses risques.
| Objectif | Destinataire | Exemple | Chemin de revenu | Risque |
|---|---|---|---|---|
| Capture de lead | Lecteur qui demande un PDF | Lien de téléchargement, guide associé | PDF gratuit puis produits | Journaliser consentement et désabonnement |
| Onboarding | Client ou participant | Guide de démarrage, checklist | Templates, cours, support | Ne pas transformer un reçu en publicité agressive |
| Suivi consultation | Prospect qualifié | Notes, proposition, prochaine date | Formation et consulting | Personnaliser depuis la vraie conversation |
| Réactivation | Lecteur consentant mais inactif | Retour d’expérience ou grosse mise à jour | Produit ou consultation | Surveiller fréquence, bounces et opt-outs |
Les termes doivent être simples. SPF est un enregistrement DNS qui indique quels serveurs peuvent envoyer pour votre domaine. DKIM ajoute une signature pour vérifier que le message est autorisé et non modifié. DMARC indique la politique à appliquer si SPF ou DKIM ne s’alignent pas. Un bounce est un échec de livraison. Un rate limit apparaît quand le fournisseur ralentit ou rejette les requêtes parce que vous envoyez trop vite, avez atteint une limite ou devez protéger votre réputation.
Pour l’authentification et la délivrabilité, partez des sources officielles : Google email sender guidelines, Resend domain management et Twilio SendGrid domain authentication. DMARC a été mis à jour en 2026 par RFC 9989, qui remplace l’ancien RFC 7489. Pour les emails commerciaux vers les États-Unis, vérifiez aussi le guide CAN-SPAM de la FTC. Ceci reste un guide d’implémentation, pas un avis juridique.
flowchart LR
Visitor["Lecteur"]
Form["Formulaire lead"]
Consent["Journal consentement"]
Queue["Queue email"]
Provider["Resend / SendGrid"]
Inbox["Boîte mail"]
Webhook["Événements"]
Analytics["Analytics"]
Offer["Produit / formation / consulting"]
Visitor --> Form --> Consent --> Queue --> Provider --> Inbox
Provider --> Webhook --> Analytics --> Offer
Inbox --> Offer
Prompt pour Claude Code
Un prompt vague donne une fonction d’envoi. Un prompt exploitable fixe le périmètre.
Implémente l'automatisation email dans ce dépôt.
Objectifs : livraison d'un lead magnet, séquence onboarding de 3 emails, suivi consultation.
Contraintes :
- Node.js 20+ et TypeScript.
- Adapter provider interchangeable entre API style Resend et API style SendGrid.
- API keys uniquement côté serveur.
- Schemas pour lead, email job, unsubscribe et provider event.
- Retry des 429 et 5xx avec exponential backoff.
- Ne pas envoyer aux adresses désabonnées, en complaint ou en suppression.
- Mettre les hard bounces répétés en suppression list.
- Inclure texte, HTML, URL de désabonnement et expéditeur clair.
- Ajouter les liens officiels fournisseur/authentification dans README.
- Ajouter scripts exécutables et tests ciblés.
Commence par la table de conception et la liste des fichiers. Attends validation avant édition.
Starter copiable
Cet exemple utilise un fichier JSON local comme petite queue. C’est suffisant pour un test. En production, remplacez-le par Postgres, Redis, SQS, Cloud Tasks ou une queue durable avec verrouillage et audit.
{
"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("fr"),
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);
}
}
Testez d’abord avec une adresse que vous contrôlez. N’envoyez pas aux lecteurs tant que le domaine d’envoi et la route de désabonnement ne sont pas vérifiés.
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, désabonnements et analytics
Une réponse API réussie signifie seulement que le fournisseur a accepté la requête. Elle ne prouve ni lecture, ni clic, ni consentement futur. Normalisez les webhooks vers votre modèle interne.
// 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(),
});
}
Ne pilotez pas uniquement à l’ouverture. Les protections de confidentialité et le blocage d’images rendent cette métrique instable. Mesurez téléchargements, clics CTA, démarrages de formulaire de consultation, réponses, désabonnements, bounces et achats.
Cas d’usage
Premier cas : le PDF gratuit sous un article technique. Envoyez le téléchargement immédiatement, puis un email sur l’erreur de configuration la plus fréquente, puis un template produit, puis une invitation à la formation ou au consulting. Un message, une action principale, un lien de désabonnement.
Deuxième cas : l’onboarding après achat. La première séquence doit aider à démarrer, résoudre les blocages et montrer l’usage avancé. Un reçu ne doit pas devenir une publicité. Aider l’acheteur à réussir est souvent la meilleure vente additionnelle.
Troisième cas : le suivi de consultation. Le message doit contenir notes, décisions, prochaines étapes, liens utiles, délai et CTA de réservation ou proposition. Sans contexte réel, même un email demandé ressemble à du spam.
Quatrième cas : la réactivation basse fréquence. Pour les lecteurs consentants mais inactifs, envoyez seulement les grosses mises à jour, retours d’expérience et ressources nouvelles. Si les clics ne reviennent pas, réduisez ou arrêtez.
Échecs à éviter
Le premier échec est d’exposer la clé API côté navigateur. L’envoi doit rester serveur.
Le deuxième est d’envoyer depuis un domaine non authentifié. Configurez SPF, DKIM et DMARC.
Le troisième est d’ignorer bounces et désabonnements. Unsubscribe, complaint et hard bounce doivent sortir des campagnes normales.
Le quatrième est de réessayer immédiatement après un rate limit. Traitez 429 et 5xx temporaires avec backoff et rythme stable. Les limites exactes varient selon compte, plan, réputation et fournisseur destinataire.
Le cinquième est de mélanger transactionnel et promotionnel. Réinitialisation de mot de passe, reçus et alertes doivent rester clairs. Les CTA commerciaux appartiennent aux emails consentis et contextualisés.
CTA de monétisation
Le système est terminé quand le lecteur peut choisir naturellement l’étape suivante. Chez ClaudeCodeLab, les débutants commencent par le PDF gratuit, les builders consultent les produits et templates, et les équipes passent par formation et consulting pour appliquer le workflow à un vrai dépôt.
En pratique, le plus gros gain vient de la conception du consentement, des désabonnements, des bounces et des analytics CTA avant le code fournisseur. Commencez par un seul email de lead magnet, vérifiez envoi, désabonnement, bounce et clic, puis élargissez vers onboarding et suivi de consultation.
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.
À propos de l'auteur
Masa
Ingénieur spécialisé dans les workflows pratiques avec Claude Code.
Articles liés
Workflow Obsidian vers CLAUDE.md avec Claude Code
Transformer des notes Obsidian en notes CLAUDE.md concises pour reprendre les sessions sans réexpliquer.
Claude Code Revenue CTA Routing : relier articles, PDF, Gumroad et consultation
Un workflow Claude Code pour orienter les lecteurs vers PDF gratuit, Gumroad ou consultation selon l'intention.
Règles de handoff Claude Code en équipe: preuves, permissions, rollback et revenus
Un format concret pour transmettre un travail Claude Code avec preuves, permissions, rollback, PDF gratuit, Gumroad et consultation.