How to Send Transactional Emails and SMS from Webflow Cloud (SendGrid + Twilio + Postmark)

Learn how to send email and SMS from Webflow Cloud.

How to Send Transactional Emails and SMS from Webflow Cloud (SendGrid + Twilio + Postmark)

Colin Lateano
Developer Evangelist
View author profile
Colin Lateano
Developer Evangelist
View author profile
Table of contents

Learn how to send email and SMS from Webflow Cloud using SendGrid, Postmark, and Twilio via fetch.

Webflow Cloud runs on Cloudflare Workers — a fast, globally distributed runtime that's genuinely great for API routes and server-side logic. It does have one gotcha worth knowing upfront: most email and SMS SDKs (@sendgrid/mail, twilio, nodemailer) rely on node:http or node:https internally, which aren't available in the Workers runtime.

The good news is the fix is straightforward: SendGrid, Postmark, and Twilio all expose REST APIs that accept plain fetch() calls, which Workers supports natively. No SDK, no Node.js module dependency, no runtime error. Once you know the pattern, it's actually less code than using the SDKs.

This guide shows the exact fetch-based implementation for all three, with reusable helper modules you can drop into any Next.js or Astro app on Webflow Cloud.

Using Astro? The helper modules in this guide (lib/sendgrid.ts, lib/postmark.ts, lib/twilio.ts) are framework-agnostic; they're just typed fetch() wrappers. You can use them in Astro API endpoints (src/pages/api/*.ts) or server actions the same way they're used in the Next.js route handlers shown here. The environment variable and runtime patterns are identical; just swap app/api/*/route.ts for your Astro endpoint path.

What do you need to send emails and SMS from a Webflow Cloud app?

You need a deployed Webflow Cloud app, API credentials from each service you want to use, and a verified sender identity for any email provider. The environment variable setup differs slightly from standard Next.js; runtime env vars are injected at request time, not build time, which affects how you initialize clients.

Before starting, verify you have all of these in place.

A Webflow Cloud app with a Next.js API route

You need at minimum a Next.js 15+ project deployed on Webflow Cloud with at least one App Router route handler (app/api/*/route.ts) where you'll call the email or SMS helpers. If you haven't initialized a project yet, the Webflow Cloud getting started guide covers project creation and first deploy.

API credentials from your email or SMS provider

For SendGrid, you need:

You can verify a single sender address under Settings → Sender Authentication → Single Sender Verification in the SendGrid dashboard. Domain authentication is required for production volume.

For Postmark, you need:

  • An account at postmarkapp.com
  • A Server Token (found under Servers → Your Server → API Tokens)
  • A verified Sender Signature; Postmark requires the From address to match a registered signature

A single email address is sufficient for testing; a domain signature is recommended for production.

For Twilio SMS, you need:

  • An account at twilio.com
  • Your Account SID and Auth Token from the Twilio Console dashboard
  • A Twilio phone number

Free trial accounts can only send to verified phone numbers. Upgrade to a paid account to send to any number.

Runtime environment variables in Webflow Cloud

Webflow Cloud injects environment variables at request time, not at build time. This is the critical difference from standard Next.js deployments: process.env.SENDGRID_API_KEY is undefined if read at module initialization, but it's populated correctly when read inside a function body during a request.

The factory function pattern handles this correctly:

// lib/sendgrid.ts — read process.env inside the function, never at module top level
export function getSendGridKey() {
  const key = process.env.SENDGRID_API_KEY;
  if (!key) throw new Error("Missing SENDGRID_API_KEY environment variable");
  return key;
}

If you call const key = process.env.SENDGRID_API_KEY at the top of a module outside any function, it will be undefined in production. I've debugged this exact issue twice; the symptom is a 401 Unauthorized from SendGrid with an empty API key string in the logs.

6 steps to send transactional email and SMS from a Webflow Cloud app

The implementation wraps each service's REST API in a typed helper function that reads credentials at call time. This pattern works for SendGrid, Postmark, and Twilio. Once the helpers are in place, you call them from any route handler or server action in your Next.js app.

The six steps below cover credential setup, a fetch-based SendGrid helper, a Postmark helper, a Twilio SMS helper, wiring them into route handlers, and deployment.

1. Add API credentials as environment variables

Before writing any code, set the environment variables that your helpers will read.

For local development, add these to .env.local at the project root:

# SendGrid
SENDGRID_API_KEY=SG.xxxxxxxxxxxxxxxxxxxx
SENDGRID_FROM_EMAIL=hello@yourdomain.com

# Postmark (alternative to SendGrid)
POSTMARK_API_TOKEN=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
POSTMARK_FROM_EMAIL=hello@yourdomain.com

# Twilio SMS
TWILIO_ACCOUNT_SID=ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
TWILIO_AUTH_TOKEN=your_auth_token_here
TWILIO_FROM_NUMBER=+15555550100

Do not commit .env.local to version control. Confirm .env.local is in your .gitignore before continuing.

For production, add these in the Webflow Cloud dashboard. Navigate to your project → select the environment → click Environment Variables → Add variable. Toggle Secret for any sensitive value (API keys, auth tokens).

Secrets are encrypted at rest and masked in the dashboard. They're never exposed in build output or logs.

SENDGRID_FROM_EMAIL and POSTMARK_FROM_EMAIL can also be set as secrets. I prefer keeping them as secrets rather than hardcoding in source, because sender addresses change more often than developers expect during email domain migrations.

2. Build the SendGrid email helper

Create a lib/sendgrid.ts helper that calls the SendGrid v3 Mail Send API using fetch():

// lib/sendgrid.ts

interface SendEmailOptions {
  to: string;
  subject: string;
  text?: string;
  html?: string;
}

export async function sendEmailSendGrid(options: SendEmailOptions): Promise<void> {
  const apiKey = process.env.SENDGRID_API_KEY;
  const fromEmail = process.env.SENDGRID_FROM_EMAIL;

  if (!apiKey) throw new Error("Missing SENDGRID_API_KEY environment variable");
  if (!fromEmail) throw new Error("Missing SENDGRID_FROM_EMAIL environment variable");

  const { to, subject, text, html } = options;

  const content: Array<{ type: string; value: string }> = [];
  if (text) content.push({ type: "text/plain", value: text });
  if (html) content.push({ type: "text/html", value: html });

  if (content.length === 0) {
    throw new Error("SendGrid email requires at least one of: text, html");
  }

  const response = await fetch("https://api.sendgrid.com/v3/mail/send", {
    method: "POST",
    headers: {
      "Authorization": `Bearer ${apiKey}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      personalizations: [
        {
          to: [{ email: to }],
        },
      ],
      from: { email: fromEmail },
      subject,
      content,
    }),
  });

  if (!response.ok) {
    const errorBody = await response.text();
    throw new Error(`SendGrid API error ${response.status}: ${errorBody}`);
  }
}

The personalizations array is SendGrid-specific. It's a batch addressing system. You can send a single API call to multiple recipients, with different template variables for each recipient. For single-recipient transactional email, you only need one object in the array with a single to entry, as shown above.

The function reads process.env inside the function body. This is the correct pattern for Webflow Cloud; credentials are available at request time, not module initialization time.

A successful send returns HTTP 202 Accepted with an empty body. The function throws on any non-OK status, which lets the caller handle errors using standard try/catch.

3. Build the Postmark email helper

Postmark is the better choice when clients need strong deliverability for one-to-one emails, such as receipts, password resets and account alerts. Its reputation tracking is more granular than SendGrid's for low-volume transactional use.

The auth model differs: Postmark uses an X-Postmark-Server-Token header, not a Bearer token.

Create lib/postmark.ts:

// lib/postmark.ts

interface PostmarkEmailOptions {
  to: string;
  subject: string;
  textBody?: string;
  htmlBody?: string;
  messageStream?: string;
}

export async function sendEmailPostmark(options: PostmarkEmailOptions): Promise<void> {
  const token = process.env.POSTMARK_API_TOKEN;
  const fromEmail = process.env.POSTMARK_FROM_EMAIL;

  if (!token) throw new Error("Missing POSTMARK_API_TOKEN environment variable");
  if (!fromEmail) throw new Error("Missing POSTMARK_FROM_EMAIL environment variable");

  const { to, subject, textBody, htmlBody, messageStream = "outbound" } = options;

  const response = await fetch("https://api.postmarkapp.com/email", {
    method: "POST",
    headers: {
      "Accept": "application/json",
      "Content-Type": "application/json",
      "X-Postmark-Server-Token": token,
    },
    body: JSON.stringify({
      From: fromEmail,
      To: to,
      Subject: subject,
      TextBody: textBody,
      HtmlBody: htmlBody,
      MessageStream: messageStream,
    }),
  });

  if (!response.ok) {
    const errorBody = await response.json() as { Message: string; ErrorCode: number };
    throw new Error(`Postmark API error ${errorBody.ErrorCode}: ${errorBody.Message}`);
  }
}

Two things to note:

  • First, the header is X-Postmark-Server-Token, not Authorization. I've watched developers spend minutes debugging a 401 because they assumed it would follow the Bearer token pattern. It doesn't.
  • Second, the field names are PascalCase (From, To, Subject, HtmlBody, TextBody), matching Postmark's REST API spec exactly. Use the exact casing shown in Postmark's documentation; the API spec does not specify case-insensitive field-name handling for the JSON body.

The messageStream parameter defaults to "outbound" (Postmark's default transactional stream). If you've created a broadcast stream for marketing emails in the Postmark dashboard, pass its name here instead.

4. Build the Twilio SMS helper

Twilio's Messages API uses application/x-www-form-urlencoded encoding, not JSON. The authentication uses HTTP Basic Auth with Account SID as the username and Auth Token as the password, encoded with btoa().

Create lib/twilio.ts:

// lib/twilio.ts

interface SendSmsOptions {
  to: string;  // E.164 format: +15555550100
  body: string;
}

export async function sendSms(options: SendSmsOptions): Promise<{ sid: string; status: string }> {
  const accountSid = process.env.TWILIO_ACCOUNT_SID;
  const authToken = process.env.TWILIO_AUTH_TOKEN;
  const fromNumber = process.env.TWILIO_FROM_NUMBER;

  if (!accountSid) throw new Error("Missing TWILIO_ACCOUNT_SID environment variable");
  if (!authToken) throw new Error("Missing TWILIO_AUTH_TOKEN environment variable");
  if (!fromNumber) throw new Error("Missing TWILIO_FROM_NUMBER environment variable");

  const { to, body } = options;

  const credentials = btoa(`${accountSid}:${authToken}`);
  const endpoint = `https://api.twilio.com/2010-04-01/Accounts/${accountSid}/Messages.json`;

  const params = new URLSearchParams();
  params.append("To", to);
  params.append("From", fromNumber);
  params.append("Body", body);

  const response = await fetch(endpoint, {
    method: "POST",
    headers: {
      "Authorization": `Basic ${credentials}`,
      "Content-Type": "application/x-www-form-urlencoded",
    },
    body: params.toString(),
  });

  if (!response.ok) {
    const errorBody = await response.json() as { message: string; code: number };
    throw new Error(`Twilio API error ${errorBody.code}: ${errorBody.message}`);
  }

  const result = await response.json() as { sid: string; status: string };
  return { sid: result.sid, status: result.status };
}

The btoa(${accountSid}:${authToken}) encoding is the part that trips people up most often. The credentials string is accountSid + ":" + authToken; Basic Auth requires the colon separator. I've seen developers either pass only the auth token or use the API key from another Twilio service.

Both result in 401 errors that look identical in the response.

Phone numbers must be in E.164 format: +15555550100. The + prefix is required. Twilio returns a 400 if you pass 15555550100 without the plus sign.

The function returns { sid, status } because you'll want the SID for logging or receipt confirmation. The status is typically "queued" upon success; delivery is asynchronous, and you'd need a webhook to track the final "delivered" state.

5. Wire helpers into Next.js route handlers

With helpers in place, call them from any App Router route handler.

Here's a complete contact form handler that sends a confirmation email via SendGrid and an internal Slack-style notification via Twilio SMS:

// app/api/contact/route.ts
export const runtime = 'edge';
import { NextRequest, NextResponse } from "next/server";
import { sendEmailSendGrid } from "@/lib/sendgrid";
import { sendSms } from "@/lib/twilio";

export async function POST(request: NextRequest) {
  const { name, email, message } = await request.json();

  if (!name || !email || !message) {
    return NextResponse.json({ error: "Missing required fields" }, { status: 400 });
  }

  try {
    // Send confirmation to user
    await sendEmailSendGrid({
      to: email,
      subject: `Thanks for reaching out, ${name}`,
      html: `<p>Hi ${name},</p><p>We received your message and will respond within 24 hours.</p>`,
      text: `Hi ${name}, We received your message and will respond within 24 hours.`,
    });

    // Notify team via SMS
    const teamPhone = process.env.TEAM_PHONE_NUMBER;
    if (teamPhone) {
      await sendSms({
        to: teamPhone,
        body: `New contact form submission from ${name} (${email})`,
      });
    }

    return NextResponse.json({ success: true });
  } catch (error) {
    console.error("Notification error:", error);
    return NextResponse.json({ error: "Failed to send notification" }, { status: 500 });
  }
}

Add export const runtime = 'edge' to every route handler file that calls these helpers. Webflow Cloud requires this directive for API routes to build correctly on the Workers runtime. Without it, your routes will not build properly.

Here's the same pattern using Postmark for a password reset email from a server action:

// app/actions/auth.ts
"use server";

import { sendEmailPostmark } from "@/lib/postmark";

export async function requestPasswordReset(email: string) {
  const resetToken = crypto.randomUUID();

  // Store resetToken in your database here...

  await sendEmailPostmark({
    to: email,
    subject: "Reset your password",
    htmlBody: `
      <p>Click the link below to reset your password. This link expires in 1 hour.</p>
      <p><a href="${process.env.APP_BASE_URL}/reset-password?token=${resetToken}">
        Reset Password
      </a></p>
    `,
    textBody: `Reset your password: ${process.env.APP_BASE_URL}/reset-password?token=${resetToken}`,
  });

  return { success: true };
}

Server actions run in the same Workers runtime as route handlers. The "use server" directive works correctly with Webflow Cloud. There's nothing special to configure. Environment variables are accessible in the same way.

6. Deploy and verify

Deploy to Webflow Cloud with:

webflow cloud deploy

After deployment, test each helper by triggering the route or action that calls it. For email, check your inbox immediately. For SMS, check the destination phone. If using SendGrid, check the Activity → Email Activity log in the SendGrid dashboard. Each send attempt is logged with full status within seconds.

For Twilio SMS, check the Monitor → Logs → Messages view in the Twilio Console. Each message shows status progression from queued → sent → delivered.

One thing I always do before any production push: test with my own email and phone number first. SendGrid's sandbox mode only validates your JSON structure; it returns 200 OK and never actually delivers the email, consumes credits, or generates Event Webhook activity.

That makes it useful for format-checking, but useless for verifying that your credentials, sender identity, and domain authentication are working. A real send to a real inbox is the only way to verify end-to-end delivery.

What causes transactional email and SMS to fail on Webflow Cloud?

Common failures fall into three categories: SDK module errors from packages that don't work in the Workers runtime, authentication errors from incorrectly configured env vars, and delivery errors from sender verification requirements.

Here's how to diagnose and fix each one.

SDK package throws "Module not found" or "node:https is not defined"

This error means you have an email or SMS SDK imported that relies on unavailable Node.js modules. The error surfaces at runtime on Webflow Cloud even if your local dev runs cleanly.

Cause: @sendgrid/mail, twilio, postmark, nodemailer, or any package that wraps node:http/node:https internally.

Fix: Remove the SDK package from your imports and replace it with the fetch-based helper from this guide. Run npm uninstall @sendgrid/mail (or the relevant package) to remove the dependency entirely. Unused packages that import node:http can still cause errors if they're bundled.

SendGrid returns 401 Unauthorized

Cause 1:SENDGRID_API_KEY read at module top level. If you have const apiKey = process.env.SENDGRID_API_KEY, when used outside any function, evaluates to undefined in Webflow Cloud's runtime. The key isn't injected until request time.

Fix: Move all process.env reads inside function bodies. The helper pattern in Step 2 handles this correctly.

Cause 2:The API key doesn't have Mail Send permissions. A newly created API key defaults to Restricted Access. Verify whether Mail Send → Full Access in the SendGrid API Keys settings.

Cause 3:Sender not verified. SendGrid rejects sends from unverified sender addresses with a 403 Forbidden response. Complete Single Sender Verification or Domain Authentication before testing.

Twilio returns 401 or 20003 Authentication Error

Cause: btoa() called with the wrong string format. A common mistake is passing btoa(authToken) instead of btoa(${accountSid}:${authToken}).

Fix: Verify the credentials string includes the colon separator: btoa(\${accountSid}:${authToken}`). Check thatTWILIO_ACCOUNT_SID starts withAC; that prefix confirms it's the Account SID, not a secondary credential like a Messaging Service SID.

Also, confirm you're reading TWILIO_AUTH_TOKEN, not TWILIO_API_KEY. Twilio has multiple credential types; the REST API for basic message sending uses the Account SID and Auth Token from the top of the Twilio Console dashboard.

Postmark returns 422 ('Sender signature not verified' error)

Cause: The From address in your payload doesn't match a verified Sender Signature in your Postmark account. Postmark returns HTTP 422 with ErrorCode 300: "Invalid 'From' address: '[your-address]' is not a verified sender signature."

Fix: Log in to the Postmark dashboard, navigate to Sender Signatures, and verify that your From email address is registered and confirmed. The address must match exactly: hello@yourdomain.com and noreply@yourdomain.com are separate signatures.

If you recently changed POSTMARK_FROM_EMAIL, the new address needs its own signature. Postmark sends a confirmation email to verify ownership.

SMS messages are queued but not delivered

Cause 1:Trial account restriction. Free Twilio trial accounts can only send to verified caller IDs. You'll see the message stuck in queued status with an error note in the Console.

Fix: Add the destination number to Verified Caller IDs in the Twilio Console, or upgrade to a paid account.

Cause 2:Invalid E.164 format for to or from numbers. Phone numbers without the + prefix are rejected silently in some regions, or accepted but undelivered.

Fix: Ensure both To and From numbers follow E.164 format: + followed by country code and number with no spaces or hyphens.

Emails land in spam after deployment

Cause: Sending from a non-authenticated domain. SendGrid and Postmark both require Domain Authentication (DKIM/SPF records) to establish sender reputation. Without it, major inbox providers apply heavy spam filtering.

Fix: Complete Domain Authentication in SendGrid (under Settings → Sender Authentication) or add a Domain Signature in Postmark. Both services provide DNS records that you can add to your domain registrar. Full propagation takes up to 48 hours, but most records propagate within an hour.

Build complete notification flows on Webflow Cloud

The helpers in this guide cover the primitives: a confirmed email send and a dispatched SMS. Real notification flows add retry logic, delivery tracking, and multi-channel routing: send email first, fall back to SMS if the email bounces, log all sends to a database for audit trails.

For apps that need email as a user-facing feature rather than just an internal tool, the Webflow SendGrid integration page covers how SendGrid connects to Webflow's form and CMS data.

For apps where you're wrapping external APIs in a consistent interface, see the guide on building API wrappers on Webflow Cloud; the same factory-function pattern used for email helpers applies to any third-party integration.

Explore Webflow's developer documentation for deeper customization, dynamic template rendering, webhook-based delivery tracking, and Workers bindings available on Webflow Cloud.

Alex Halliday
CEO
AirOps
Learn more
Aleyda Solis
International SEO Consultant and Founder
Orainti
Learn more
Barry Schwartz
President and Owner
RustyBrick, Inc
Learn more
Chris Andrew
CEO and Cofounder
Scrunch
Learn more
Connor Gillivan
CEO and Founder
TrioSEO
Learn more
Eli Schwartz
Author
Product-led SEO
Learn more
Ethan Smith
CEO
Graphite
Learn more
Evan Bailyn
CEO
First Page Sage
Learn more
Gaetano Nino DiNardi
Growth Advisor
Learn more
Jason Barnard
CEO and Founder
Kalicube
Learn more
Kevin Indig
Growth Advisor
Learn more
Lily Ray
VP SEO Strategy & Research
Amsive
Learn more
Marcel Santilli
CEO and Founder
GrowthX
Learn more
Michael King
CEO and Founder
iPullRank
Learn more
Rand Fishkin
CEO and Cofounder
SparkToro, Alertmouse, & Snackbar Studio
Learn more
Stefan Katanic
CEO
Veza Digital
Learn more
Steve Toth
CEO
Notebook Agency
Learn more
Sydney Sloan
CMO
G2
Learn more

Frequently asked questions

Can I use Resend instead of SendGrid or Postmark on Webflow Cloud?

Yes. Resend (npm install resend) is designed for edge runtimes and has official Cloudflare Workers support. Its SDK uses fetch() internally, so it works on Webflow Cloud without any modifications, unlike @sendgrid/mail or nodemailer.

Why does@sendgrid/mailwork in local dev but fail on Webflow Cloud?

Local next dev runs on Node.js, which has full node:http and node:https support. The @sendgrid/mail package uses those modules internally, so everything works. Webflow Cloud runs on Cloudflare Workers, where those modules aren't available. The adapter doesn't polyfill them because they require TCP sockets, which Workers doesn't support at all. The fix is replacing the SDK with a direct fetch() call to SendGrid's REST API.

Do I need a different code for Webflow Cloud vs a standard Next.js deployment?

Only for packages that use Node.js HTTP modules, and only the network call itself, not the logic around it. The factory function pattern (reading process.env inside functions rather than at the module top level) also differs from some standard Next.js deployments, where build-time env vars are common. Everything else (App Router structure, server actions, middleware, TypeScript types) is identical.

Can I send emails from Next.js server components on Webflow Cloud?

Technically, yes, but it's a bad pattern. Server components render on every request, and triggering a side effect (sending an email) from a render function makes the behavior non-idempotent; the email fires on every page load or re-render, including during React's concurrent rendering optimizations. Use route handlers (app/api/*/route.ts) or server actions ("use server") for any operation that has side effects, such as sending email or SMS.

Read now

Last Updated
April 19, 2026
Category

Related articles

How to Embed an Instagram Feed on Webflow and Stop Double-Posting Instagram Content
How to Embed an Instagram Feed on Webflow and Stop Double-Posting Instagram Content

How to Embed an Instagram Feed on Webflow and Stop Double-Posting Instagram Content

How to Embed an Instagram Feed on Webflow and Stop Double-Posting Instagram Content

Development
By
Colin Lateano
,
,
Read article
How to Prevent Page Scroll When a Modal Is Open in Webflow + the iOS Safari Fix
How to Prevent Page Scroll When a Modal Is Open in Webflow + the iOS Safari Fix

How to Prevent Page Scroll When a Modal Is Open in Webflow + the iOS Safari Fix

How to Prevent Page Scroll When a Modal Is Open in Webflow + the iOS Safari Fix

Development
By
Colin Lateano
,
,
Read article
How Do You Add a GDPR-Compliant Cookie Consent Banner to Webflow Using Cookiebot?
How Do You Add a GDPR-Compliant Cookie Consent Banner to Webflow Using Cookiebot?

How Do You Add a GDPR-Compliant Cookie Consent Banner to Webflow Using Cookiebot?

How Do You Add a GDPR-Compliant Cookie Consent Banner to Webflow Using Cookiebot?

Development
By
Colin Lateano
,
,
Read article
How to Link Webflow Forms to HubSpot Without Losing Your Form Design
How to Link Webflow Forms to HubSpot Without Losing Your Form Design

How to Link Webflow Forms to HubSpot Without Losing Your Form Design

How to Link Webflow Forms to HubSpot Without Losing Your Form Design

Development
By
Colin Lateano
,
,
Read article

verifone logomonday.com logospotify logoted logogreenhouse logoclear logocheckout.com logosoundcloud logoreddit logothe new york times logoideo logoupwork logodiscord logo
verifone logomonday.com logospotify logoted logogreenhouse logoclear logocheckout.com logosoundcloud logoreddit logothe new york times logoideo logoupwork logodiscord logo

Get started for free

Try Webflow for as long as you like with our free Starter plan. Purchase a paid Site plan to publish, host, and unlock additional features.

Get started — it’s free
Watch demo

Try Webflow for as long as you like with our free Starter plan. Purchase a paid Site plan to publish, host, and unlock additional features.