Depuis l'arrêt de Webflow Logic en juin 2025, envoyer un véritable e-mail transactionnel depuis un formulaire Webflow nécessite un vrai backend. Webflow Cloud fait de ce backend un Route Handler : quelques dizaines de lignes de TypeScript qui appellent l'API de Postmark et envoient deux e-mails au moment où un formulaire est soumis.
L'e-mail transactionnel est l'une des premières choses que les clients me demandent d'ajouter une fois qu'un site Webflow dépasse le stade de la brochure. Les notifications de formulaire intégrées de Webflow sont envoyées au propriétaire du site, sans aucun contrôle sur l'adresse d'expéditeur, le modèle ou le texte. Webflow Logic, qui gérait une partie de cette automatisation, a été abandonné le 27 juin 2025.
L'approche actuelle consiste en un Route Handler d'application Webflow Cloud qui reçoit les données du formulaire, appelle l'API HTTP de Postmark, et envoie des e-mails à l'image de la marque dans les deux sens : une confirmation à la personne qui a soumis le formulaire et une notification à votre équipe.
Dans ce guide, nous couvrons chaque étape : vérifier votre domaine d'expéditeur dans Postmark, configurer les identifiants, écrire un helper d'e-mail réutilisable, construire le Route Handler, et le connecter à un formulaire Webflow avec du JavaScript côté client.
De quoi avez-vous besoin pour envoyer des e-mails transactionnels depuis Webflow Cloud ?
Vous avez besoin d'un compte Postmark, d'un domaine d'expéditeur vérifié, et d'une application Webflow Cloud déjà initialisée avant d'écrire le moindre code.
Assurez-vous que ces prérequis sont remplis avant de commencer :
- Un compte Postmark : le niveau gratuit envoie 100 e-mails par mois, ce qui suffit pour le développement et une production à faible volume
- Un domaine que vous possédez, avec accès DNS : Postmark exige l'ajout d'enregistrements DNS (DKIM + Return-Path) pour vérifier votre domaine d'envoi
- Uneapplication Webflow Cloud : exécutez
webflow auth login && webflow cloud initsi vous ne l'avez pas encore fait. - Un site Webflow avec au moins un formulaire nécessitant une gestion d'e-mail personnalisée
Une chose à savoir avant de commencer : le package npm officiel postmark ne fonctionne pas sur Cloudflare Workers. La documentation de Cloudflare l'indique directement : « Postmark's JavaScript library is currently not supported on Workers. » L'API HTTP, en revanche, fonctionne parfaitement via fetch(). C'est celle-ci que ce guide utilise.
5 étapes pour envoyer des e-mails transactionnels depuis Webflow Cloud avec Postmark
Postmark s'occupe de la livraison. L'application Webflow Cloud s'occupe de la validation, du routage et de la mise en forme de la réponse. Ces étapes construisent l'intégration dans l'ordre suivant : vérification du domaine d'expéditeur, configuration des identifiants, un helper d'e-mail réutilisable, et un Route Handler vers lequel le formulaire Webflow envoie ses données.
1. Vérifiez votre domaine d'expéditeur dans Postmark
Postmark bloque l'envoi depuis des domaines non vérifiés. Cette étape ne peut pas être ignorée, et c'est la raison la plus fréquente de l'échec du premier envoi. Configurez-la avant d'écrire le moindre code.
Connectez-vous à Postmark et allez dans Sender Signatures → Add Domain or Signature → Add Domain. Saisissez votre domaine et cliquez sur Verify Domain.
Postmark affiche deux enregistrements DNS à ajouter :
- Un enregistrement DKIM (type TXT)
- Un enregistrement Return-Path (type CNAME)
Ajoutez les deux enregistrements chez votre fournisseur DNS. Dans Cloudflare, allez dans DNS → Records → Add record et copiez chaque valeur exactement. Une fois que les enregistrements se propagent (généralement moins de 10 minutes sur Cloudflare), retournez sur Postmark et cliquez sur les boutons Verify.
Une fois le domaine vérifié, récupérez votre token API serveur. Allez dans Servers → My First Server → API Tokens et copiez le token serveur. Cette valeur est incluse dans l'en-tête d'authentification X-Postmark-Server-Token pour chaque requête API.
Point de contrôle : depuis le tableau de bord Postmark, utilisez le bouton Test pour envoyer un e-mail de test. S'il est livré, le domaine est vérifié et le token fonctionne.
2. Ajoutez les identifiants Postmark à votre environnement Webflow Cloud
Votre application Cloud a besoin de deux variables d'environnement.
Ajoutez-les à .env.local pour le développement local :
# .env.local
POSTMARK_SERVER_TOKEN=your_server_token_here
POSTMARK_FROM_EMAIL=hello@yourdomain.com
OWNER_EMAIL=you@yourdomain.com
Dans les paramètres de votre site Webflow, ouvrez Webflow Cloud, sélectionnez votre environnement, et ajoutez les mêmes variables sous Environment Variables.
Pour les tests locaux sans envoyer de vrais e-mails, Postmark fournit un token de test spécial : POSTMARK_API_TEST. Remplacez localement votre vrai token par cette valeur pour valider le format de la requête sans déclencher de livraison réelle.
Point de contrôle : exécutez next dev en local et confirmez que process.env.POSTMARK_SERVER_TOKEN se résout bien à votre token.
3. Créez le helper d'e-mail Postmark
Créez lib/postmark.ts dans votre application Cloud. Ce module gère l'appel HTTP vers l'endpoint /email de Postmark et met la requête dans le format attendu par l'API.
Voici ce qu'il faut faire :
// lib/postmark.ts
const POSTMARK_API = 'https://api.postmarkapp.com'
interface EmailParams {
to: string
from: string
subject: string
htmlBody: string
textBody?: string
replyTo?: string
tag?: string
messageStream?: string
}
interface PostmarkResponse {
ErrorCode: number
Message: string
MessageID: string
SubmittedAt: string
To: string
}
export async function sendEmail(params: EmailParams): Promise<PostmarkResponse> {
const token = process.env.POSTMARK_SERVER_TOKEN
if (!token) throw new Error('POSTMARK_SERVER_TOKEN is not set')
const res = await fetch(`${POSTMARK_API}/email`, {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
'X-Postmark-Server-Token': token,
},
body: JSON.stringify({
From: params.from,
To: params.to,
Subject: params.subject,
HtmlBody: params.htmlBody,
TextBody: params.textBody ?? params.htmlBody.replace(/<[^>]+>/g, ''),
ReplyTo: params.replyTo,
Tag: params.tag,
TrackOpens: true,
MessageStream: params.messageStream ?? 'outbound',
}),
})
if (!res.ok) {
const error = (await res.json()) as PostmarkResponse
throw new Error(`Postmark error ${error.ErrorCode}: ${error.Message}`)
}
return res.json() as Promise<PostmarkResponse>
}
Je garde l'appel Postmark dans un helper dédié plutôt que directement dans le Route Handler. Quand la même application envoie plusieurs types d'e-mails (confirmation de contact, reçu de commande, réinitialisation de mot de passe), chaque Route Handler importe sendEmail() avec ses propres paramètres. Aucune logique fetch dupliquée entre les fichiers.
Le repli TextBody supprime automatiquement les balises HTML pour générer une version en texte brut. En production, écrivez un vrai corps en texte brut : les clients de messagerie qui n'affichent que du texte (y compris de nombreux filtres anti-spam d'entreprise) offrent une meilleure expérience qu'avec du HTML simplement dépouillé.
Point de contrôle : depuis un script de test rapide, appelez sendEmail() avec POSTMARK_SERVER_TOKEN réglé sur POSTMARK_API_TEST. Vous devriez récupérer { ErrorCode: 0, Message: "Test job accepted" }.
4. Créez le Route Handler du formulaire de contact
Créez app/api/contact/route.ts. Ce Route Handler reçoit une soumission de formulaire en JSON, valide les données, envoie deux e-mails via Postmark (une confirmation à la personne qui a soumis le formulaire et une notification au propriétaire du site), et renvoie une réponse JSON.
Voici le contenu :
// app/api/contact/route.ts
export const runtime = 'edge'
import { sendEmail } from '@/lib/postmark'
const SENDER = process.env.POSTMARK_FROM_EMAIL ?? 'hello@yourdomain.com'
const OWNER_EMAIL = process.env.OWNER_EMAIL ?? 'you@yourdomain.com'
interface ContactPayload {
name: string
email: string
message: string
}
function isValidEmail(email: string): boolean {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)
}
export async function POST(request: Request) {
let body: ContactPayload
try {
body = (await request.json()) as ContactPayload
} catch {
return Response.json({ error: 'Invalid JSON' }, { status: 400 })
}
const { name, email, message } = body
if (!name?.trim() || !email?.trim() || !message?.trim()) {
return Response.json(
{ error: 'Name, email, and message are required' },
{ status: 422 }
)
}
if (!isValidEmail(email)) {
return Response.json({ error: 'Invalid email address' }, { status: 422 })
}
try {
// Confirmation to the submitter
const confirmation = await sendEmail({
to: email,
from: SENDER,
replyTo: OWNER_EMAIL,
subject: `Thanks for reaching out, ${name}`,
tag: 'contact-confirmation',
htmlBody: `
<p>Hi ${name},</p>
<p>Thanks for your message. I'll get back to you within one business day.</p>
<p>Here's what you sent:</p>
<blockquote>${message}</blockquote>
`,
})
// Notification to the site owner
await sendEmail({
to: OWNER_EMAIL,
from: SENDER,
replyTo: email,
subject: `New contact from ${name}`,
tag: 'contact-notification',
htmlBody: `
<p><strong>Name:</strong> ${name}</p>
<p><strong>Email:</strong> <a href="mailto:${email}">${email}</a></p>
<p><strong>Message:</strong></p>
<p>${message}</p>
`,
})
return Response.json({
success: true,
messageId: confirmation.MessageID,
})
} catch (err) {
console.error('Email send failed:', err)
return Response.json({ error: 'Failed to send email' }, { status: 502 })
}
}
Le handler valide avant d'appeler Postmark. Un nom vide ou une adresse e-mail malformée n'atteignent jamais l'API, ce qui signifie aucun appel API gaspillé et aucune réponse 422 confuse de Postmark à analyser.
Je règle ReplyTo sur l'e-mail du propriétaire pour la confirmation, et sur l'e-mail de la personne qui a soumis le formulaire pour la notification. Ainsi, quand le propriétaire du site clique sur répondre dans son client de messagerie, il joint directement la personne qui a soumis le formulaire.
Point de contrôle : vous pouvez tester l'endpoint avec cURL :
curl -X POST http://localhost:3000/app/api/contact \
-H "Content-Type: application/json" \
-d '{"name":"Test User","email":"test@example.com","message":"Hello"}'
Avec POSTMARK_SERVER_TOKEN=POSTMARK_API_TEST, vous devriez recevoir { "success": true, "messageId": "..." }. Avec votre vrai token, vérifiez le journal d'activité de Postmark pour confirmer que les deux e-mails apparaissent.
5. Connectez le formulaire Webflow
Ajoutez ceci dans un bloc de code personnalisé (Before </body>) sur toute page comportant un formulaire de contact. Le formulaire nécessite trois champs de saisie avec les attributs data-contact-name, data-contact-email et data-contact-message, ainsi qu'un élément de statut avec data-contact-status.
Ajoutez un attribut data-contact-form à l'élément de formulaire lui-même :
(function () {
const form = document.querySelector('[data-contact-form]')
const status = document.querySelector('[data-contact-status]')
if (!form) return
form.addEventListener('submit', async function (e) {
e.preventDefault()
const name = form.querySelector('[data-contact-name]')?.value.trim()
const email = form.querySelector('[data-contact-email]')?.value.trim()
const message = form.querySelector('[data-contact-message]')?.value.trim()
if (!name || !email || !message) {
if (status) status.textContent = 'Please fill in all fields.'
return
}
const submitBtn = form.querySelector('[type="submit"]')
if (submitBtn) submitBtn.disabled = true
if (status) status.textContent = 'Sending...'
try {
const res = await fetch('/app/api/contact', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name, email, message }),
})
const data = await res.json()
if (data.success) {
if (status) status.textContent = 'Message sent. Check your inbox for a confirmation.'
form.reset()
} else {
if (status) status.textContent = data.error ?? 'Something went wrong. Please try again.'
}
} catch {
if (status) status.textContent = 'Network error. Please try again.'
} finally {
if (submitBtn) submitBtn.disabled = false
}
})
})()
Je désactive le bouton de soumission pendant que la requête est en cours. Sans cela, une double soumission enverrait deux e-mails de confirmation à la même adresse. L'élément data-contact-status donne un retour aux utilisateurs sans déclencher le rechargement de page que provoquerait la gestion de formulaire par défaut de Webflow.
Point de contrôle : publiez votre site Webflow, remplissez le formulaire et soumettez-le. Le message de statut devrait se mettre à jour, et vous devriez recevoir à la fois l'e-mail de confirmation et la notification au propriétaire.
Qu'est-ce qui fait échouer les envois d'e-mails Postmark sur Webflow Cloud ?
La plupart des échecs se répartissent en trois catégories : le SDK npm importé alors qu'il ne devrait pas l'être, des signatures d'expéditeur non vérifiées, et des adresses destinataires inactives.
Voici à quoi chacun ressemble et comment y remédier.
Le package npm postmark lève une erreur à l'importation
Si vous voyez « Cannot find module 'events' » ou « TypeError: Class extends value undefined » au chargement de votre Route Handler, le package npm postmark est installé. Retirez-le avec npm uninstall postmark et utilisez le helper basé sur fetch() de l'étape 3.
La documentation Cloudflare Workers indique explicitement que la bibliothèque JavaScript de Postmark n'est pas prise en charge sur Workers. L'API HTTP couvre chaque opération de ce guide.
Postmark renvoie ErrorCode 400 ou 401
ErrorCode 400 signifie qu'aucune Sender Signature n'existe dans Postmark pour l'adresse From ; elle n'a pas encore été créée. ErrorCode 401 signifie que la Sender Signature existe, mais que l'e-mail de confirmation n'a pas encore été cliqué.
Les deux se résolvent dans le tableau de bord Postmark : ajoutez soit l'adresse From comme nouvelle Sender Signature, soit complétez la vérification du domaine sous Sender Signatures. Un seul domaine vérifié couvre n'importe quelle adresse sur ce domaine, donc hello@yourdomain.com et no-reply@yourdomain.com fonctionnent tous les deux une fois le domaine vérifié.
Postmark renvoie ErrorCode 406
ErrorCode 406 signifie que l'adresse destinataire est inactive. Postmark marque les destinataires comme inactifs après un rejet définitif (hard bounce) ou une plainte pour spam. Je gère le 406 dans le Route Handler en renvoyant une erreur visible par l'utilisateur : « Cette adresse e-mail a précédemment été rejetée. Merci d'utiliser une autre adresse. »
Renvoyer ne fonctionnera pas. Les destinataires inactifs nécessitent une réactivation manuelle via le journal d'activité de Postmark.
Que construire une fois l'e-mail transactionnel opérationnel sur Webflow Cloud
Si vous préférez envoyer via SendGrid, la même structure de Route Handler s'applique, et notre guide de formulaire de contact SendGrid la couvre, y compris la validation côté serveur et la protection anti-spam par honeypot que ce tutoriel n'aborde pas.
Une fois l'e-mail opérationnel, l'ajout naturel suivant est les templates Postmark. Plutôt que de construire le HTML des e-mails à l'intérieur du Route Handler, l'API Templates de Postmark vous permet de gérer le texte des e-mails dans le tableau de bord Postmark et de le rendre avec des variables au moment de l'envoi : un appel sendTemplate() qui poste vers /email/withTemplate avec { TemplateAlias: 'contact-confirmation', TemplateModel: { name, message } }. Le HTML reste hors de la base de code et peut être modifié sans déploiement.
Découvrez Webflow + Postmark pour une liste complète des options d'intégration, ou consultez la documentation Webflow Cloud si vous configurez votre application Cloud pour la première fois.
Questions fréquentes
Puis-je utiliser les templates d'e-mail intégrés de Postmark ?
Oui. L'API Templates de Postmark accepte un TemplateAlias (l'identifiant du modèle dans le tableau de bord) et un TemplateModel (un objet JSON de variables à substituer). Remplacez sendEmail() par une fonction sendTemplate() qui poste vers /email/withTemplate avec { TemplateAlias: 'your-alias', TemplateModel: { name, message } }. Les templates sont créés et modifiés dans le tableau de bord Postmark, sans qu'un déploiement de code soit nécessaire pour mettre à jour le texte.
Comment déclencher des e-mails depuis la soumission de formulaire intégrée de Webflow ?
Les formulaires intégrés de Webflow déclenchent un webhook form_submission quand un visiteur soumet un formulaire. Configurez un webhook dans Webflow Settings → Integrations → Webhooks avec le déclencheur form_submission pointant vers l'URL de votre Route Handler. Le Route Handler reçoit les données des champs du formulaire en JSON et appelle sendEmail() exactement comme ci-dessus. La seule différence est la forme de la charge utile entrante : le webhook de Webflow livre un objet fieldData imbriqué plutôt que des champs JSON plats.
Comment envoyer à plusieurs destinataires ?
Postmark accepte des adresses séparées par des virgules dans les champs To, Cc et Bcc, avec une limite combinée de 50 destinataires par message. Pour des envois à plus de 50 destinataires, utilisez l'endpoint batch de Postmark à /email/batch, qui accepte jusqu'à 500 messages par appel API.
Comment tester sans envoyer de vrais e-mails ?
Réglez POSTMARK_SERVER_TOKEN=POSTMARK_API_TEST dans .env.local. Les requêtes vont vers l'API de Postmark, votre format JSON est validé, et vous récupérez un MessageID. Aucun e-mail n'est livré. C'est la bonne façon d'écrire des tests d'intégration sur le flux d'e-mail sans envoyer vers de vraies boîtes de réception.


