# SendGrid

- Tool: FlutterFlow
- Difficulty: Intermediate
- Time required: 40 minutes
- Last updated: July 2026

## TL;DR

Connect FlutterFlow to SendGrid by routing transactional email through a Firebase Cloud Function that holds your SendGrid API key. FlutterFlow's API Call points at the Cloud Function URL — not directly at api.sendgrid.com — so the Full-Access API key never ships inside the compiled Flutter app. Use SendGrid Dynamic Templates to keep email copy off your device entirely.

## Why Connect FlutterFlow to SendGrid?

Transactional email is one of the first features every production app needs: welcome messages after sign-up, order confirmations, password-reset notices, and receipts. SendGrid is the most widely used infrastructure for these triggered emails, offering a generous free tier of 100 emails per day, strong deliverability, and a Dynamic Templates system that lets you manage all email copy — subject lines, body HTML, and personalisation tokens — inside the SendGrid dashboard rather than hardcoded in your app.

The critical FlutterFlow constraint is that the SendGrid API key cannot be placed anywhere inside the compiled Flutter app. FlutterFlow generates native iOS, Android, and web code that runs entirely on the user's device. Any key in a FlutterFlow API Call header is compiled into the app binary, and APKs and IPAs can be decompiled with free tools to extract every string. The correct architecture is a Firebase Cloud Function that holds the key in Firebase Secret Manager or an environment variable, injects the `Authorization: Bearer SG.xxx` header server-side, and forwards the request to `https://api.sendgrid.com/v3/mail/send`. Your FlutterFlow API Call only sends non-secret data: recipient email, template ID, and the personalisation values. Note that Retool ships a native SendGrid connector — FlutterFlow does not, so this proxy pattern is the standard approach.

SendGrid's free tier allows 100 emails per day with no expiry. Paid plans start at around $19.95 per month. Before writing any code, verify a sender email or domain in the SendGrid dashboard — every send from an unverified sender returns a 403 error, so this step must be done first.

## Before you start

- A SendGrid account with a verified sender email address or verified sending domain (required before any email can be delivered)
- A SendGrid API key with at least the Mail Send permission scope (create in Settings → API Keys → Restricted Access)
- A Firebase project on the Blaze (pay-as-you-go) plan — Cloud Functions that make outbound HTTP calls require Blaze
- At least one SendGrid Dynamic Template created with your email design and noting its template_id
- A FlutterFlow project where you want to trigger transactional emails

## Step-by-step guide

### 1. Verify your sender identity and create a scoped SendGrid API key

Log in to your SendGrid dashboard and navigate to Settings → Sender Authentication. You have two options: verify a single email address (fastest, takes one email click) or verify a sending domain (more professional, requires DNS record changes at your domain registrar). Single sender verification is fine for development; domain authentication is strongly recommended for production because it improves deliverability and removes the 'via sendgrid.net' annotation from the sender field.

Once your sender is verified, go to Settings → API Keys and click Create API Key. Give it a name like 'FlutterFlow App — Mail Send'. Select Restricted Access, then expand the Mail Send section and toggle it to Full Access. Scroll down and click Create & View. Copy the key immediately — SendGrid only shows it once. Do not paste it into FlutterFlow or any frontend file. Store it in a password manager or go straight to the next step and add it to Firebase Secret Manager.

Also go to Email API → Dynamic Templates and create your first template. Click Create a Dynamic Template, name it (for example, 'Welcome Email'), and click Add Version to open the drag-and-drop design editor. Design your email and insert Handlebars tokens like `{{name}}` or `{{order_id}}` wherever you want FlutterFlow to inject dynamic values. When you save, note the template_id — it starts with `d-` followed by a long UUID string. You will pass this ID from FlutterFlow as a variable.

**Expected result:** You have a verified sender, a scoped API key stored securely (not in FlutterFlow), and a Dynamic Template with a template_id beginning with `d-`.

### 2. Deploy a Firebase Cloud Function that proxies the SendGrid mail send endpoint

In the Firebase console, ensure your project is on the Blaze plan (Functions → Upgrade if needed). Open the Functions dashboard and note that you will deploy a simple Node.js HTTP function. This function receives a POST request from FlutterFlow containing the recipient email, the template ID, and the dynamic template data object. It then injects the `Authorization: Bearer SG.xxx` header from an environment variable and forwards the request to `https://api.sendgrid.com/v3/mail/send`.

To add the API key securely, use Firebase Secret Manager: in the Firebase CLI run `firebase functions:secrets:set SENDGRID_API_KEY` and paste your key when prompted. Alternatively, set it as a Firebase environment configuration value. The key lives exclusively on Google's infrastructure — it is never in your source code file and never transmitted to the Flutter client.

Deploy the function using the Firebase CLI (`firebase deploy --only functions`) or through the Firebase console's inline editor for simple functions. Once deployed, the console shows you the HTTPS trigger URL — copy it. This URL is what you will paste into FlutterFlow's API Call base URL field. The URL itself is not a secret; it is safe to include in your app because without the SendGrid key the function is useless to an attacker, and you can add Firebase App Check to restrict which apps can invoke your functions.

```
const functions = require('firebase-functions');
const { defineSecret } = require('firebase-functions/params');

const sendgridApiKey = defineSecret('SENDGRID_API_KEY');

exports.sendEmail = functions
  .runWith({ secrets: ['SENDGRID_API_KEY'] })
  .https.onRequest(async (req, res) => {
    // Allow CORS for FlutterFlow web builds
    res.set('Access-Control-Allow-Origin', '*');
    if (req.method === 'OPTIONS') {
      res.set('Access-Control-Allow-Methods', 'POST');
      res.set('Access-Control-Allow-Headers', 'Content-Type');
      return res.status(204).send('');
    }

    if (req.method !== 'POST') {
      return res.status(405).json({ error: 'Method not allowed' });
    }

    const { to_email, to_name, template_id, dynamic_data } = req.body;

    if (!to_email || !template_id) {
      return res.status(400).json({ error: 'to_email and template_id are required' });
    }

    const payload = {
      personalizations: [{
        to: [{ email: to_email, name: to_name || '' }],
        dynamic_template_data: dynamic_data || {},
      }],
      from: {
        email: 'hello@yourdomain.com',  // Your verified sender
        name: 'Your App Name',
      },
      template_id,
    };

    try {
      const response = await fetch('https://api.sendgrid.com/v3/mail/send', {
        method: 'POST',
        headers: {
          Authorization: `Bearer ${process.env.SENDGRID_API_KEY}`,
          'Content-Type': 'application/json',
        },
        body: JSON.stringify(payload),
      });

      if (response.status === 202) {
        return res.status(200).json({ success: true });
      } else {
        const errorBody = await response.text();
        console.error('SendGrid error:', response.status, errorBody);
        return res.status(response.status).json({ error: errorBody });
      }
    } catch (err) {
      console.error('Function error:', err);
      return res.status(500).json({ error: 'Failed to send email' });
    }
  });
```

**Expected result:** The Cloud Function is deployed and you can test it with a direct POST request (using a tool like Postman or curl) to confirm it delivers an email to your inbox.

### 3. Create a FlutterFlow API Call group pointing at your Cloud Function

In FlutterFlow, click API Calls in the left navigation panel and then click + Add → Create API Group. Name it SendGridProxy. In the Base URL field paste the HTTPS URL of your Cloud Function (for example, `https://us-central1-your-project.cloudfunctions.net/sendEmail`). Do not add any Authorization header here — the API key lives inside the Cloud Function, not in FlutterFlow.

Inside the group, click + Add to create an API Call. Name it SendTransactionalEmail. Set the method to POST. Leave the endpoint field blank (the base URL is the full function URL). Navigate to the Body tab and set the body type to JSON. Add the following JSON body with FlutterFlow variable syntax:

```json
{
  "to_email": "{{to_email}}",
  "to_name": "{{to_name}}",
  "template_id": "{{template_id}}",
  "dynamic_data": {{dynamic_data}}
}
```

Go to the Variables tab and add four variables: `to_email` (String, required), `to_name` (String, optional), `template_id` (String, default value = your template's `d-` ID), and `dynamic_data` (JSON Object). The template_id can have a default value so you do not need to pass it every time for the same email type.

Click Response & Test. Set the test variables with a real email address and your template_id and click Test API Call. If the Cloud Function is working you will receive the email in your inbox within a few seconds and the response panel in FlutterFlow will show a 200 status. Save the API Call. The integration is now ready to be wired to actions.

```
{
  "api_group": "SendGridProxy",
  "method": "POST",
  "base_url": "https://us-central1-YOUR_PROJECT.cloudfunctions.net/sendEmail",
  "body_type": "JSON",
  "body": {
    "to_email": "{{to_email}}",
    "to_name": "{{to_name}}",
    "template_id": "{{template_id}}",
    "dynamic_data": "{{dynamic_data}}"
  },
  "variables": [
    { "name": "to_email", "type": "String", "required": true },
    { "name": "to_name", "type": "String", "default": "" },
    { "name": "template_id", "type": "String", "default": "d-YOUR_TEMPLATE_ID" },
    { "name": "dynamic_data", "type": "JSON Object", "default": "{}" }
  ]
}
```

**Expected result:** The SendGridProxy API Group is created, the test delivers a real email to your inbox, and the API Call is ready to bind to FlutterFlow actions.

### 4. Wire the email action to a button or sign-up event in the Action Flow Editor

Now you connect the API Call to a real user action in your app. The two most common triggers are a button tap (for a contact form or manual send) and an authenticated sign-up event (for a welcome email). In both cases the process in FlutterFlow is the same: open the widget's Actions panel, click + Add Action, and navigate to Backend/Database → API Request → select SendGridProxy → SendTransactionalEmail.

For a contact form button: bind `to_email` to the text field widget that holds the user's email address. Bind `to_name` to the name field. Set `template_id` to your contact-notification template's ID. For `dynamic_data`, click the JSON Editor icon and build the object using your form field values — for example, `{"name": "[Name Field Value]", "message": "[Message Field Value]"}`. FlutterFlow lets you reference widget states and page variables here.

For a sign-up welcome email: add this action as the second step in your sign-up success flow (after the 'Create Account' action). Bind `to_email` to the email field the user typed during registration. For `dynamic_data` pass `{"name": "[first name field]"}` so the welcome template can greet them by name.

In both cases, add a conditional action after the API Call to handle success and failure: check the API response status (200 = success) and show a SnackBar or navigate to a confirmation screen. If the status is not 200, show an error message rather than silently failing — this is especially important for forms where the user needs to know their message was received.

**Expected result:** Tapping the button or completing sign-up triggers the Cloud Function, delivers the email within seconds, and the app shows a success or error state based on the API response.

### 5. Handle edge cases: daily cap exceeded, unverified sender, and 403 errors

Three failure modes affect nearly every SendGrid integration in FlutterFlow, and handling them gracefully turns a brittle prototype into a production-ready feature.

First, the SendGrid free tier silently stops delivering emails once the 100 emails/day limit is exceeded. The API still returns a 202 Accepted response — it queues the email — but the email may never actually deliver. If you are testing heavily or already have real users, monitor your SendGrid dashboard's Activity Feed (Email Activity → Activity Feed) and upgrade before hitting the cap. In your Cloud Function, log the HTTP status of every SendGrid response so you can monitor from the Firebase console.

Second, unverified senders cause every send attempt to fail with HTTP 403 and an error body containing 'The from address does not match a verified Sender Identity'. This is the most common error for developers who skip Step 1 and jump straight to code. Double-check that the `from.email` in your Cloud Function payload exactly matches (including capitalisation) the email address you verified in SendGrid Sender Authentication.

Third, the `dynamic_template_data` object must match the Handlebars tokens you used in the template design. If your template uses `{{first_name}}` but your FlutterFlow action sends `{"name": "Alice"}`, the token in the email will render blank rather than throwing an error. Preview your template in SendGrid's template editor with sample data before testing from FlutterFlow to catch these mismatches early.

**Expected result:** Your integration gracefully handles the 100/day cap (by surfacing the limit in your Firebase logs), always sends from a verified address, and template tokens render correctly because they match the Handlebars variable names.

## Best practices

- Never put the SendGrid API key in a FlutterFlow API Call header — it will be embedded in the compiled app binary and is trivially extractable from any installed APK or IPA.
- Use SendGrid Dynamic Templates rather than building HTML email bodies in your Cloud Function or Flutter code — this keeps email copy manageable and lets designers iterate without touching code.
- Create a scoped API key with only the Mail Send permission rather than a Full-Access key — if the key is ever exposed, the damage is limited to email sending and cannot affect your contacts, suppressions, or account settings.
- Monitor your SendGrid Activity Feed regularly — it is the only way to see whether emails are being delivered, bounced, or silently dropped due to the free-tier daily cap.
- Add CORS headers to your Cloud Function so that FlutterFlow web builds can call it from the browser without XMLHttpRequest errors blocking the send action.
- Use the `reply_to` field in your SendGrid payload when sending contact-form notifications — set it to the user's email address so you can reply directly without manually copying the address.
- Log every SendGrid HTTP response status in your Cloud Function with `console.log` so you can diagnose issues from the Firebase console without needing to reproduce them from a device.
- Verify your sending domain rather than a single email address in production — domain authentication removes the 'via sendgrid.net' annotation and significantly improves deliverability rates.

## Use cases

### Welcome email sent automatically on user sign-up

When a user completes registration in a FlutterFlow app using Firebase Auth or Supabase Auth, an on-sign-up action fires a Cloud Function that calls SendGrid's mail send endpoint with a welcome Dynamic Template. The template includes the user's name in the subject line and body using Handlebars-style `{{name}}` tokens that FlutterFlow passes as `dynamic_template_data`.

Prompt example:

```
When a new user signs up, I want them to immediately receive a welcome email with their name in the subject line. The email should explain the three main features of the app and have a call-to-action button linking to the app's home screen.
```

### Order confirmation email triggered from a purchase screen

A FlutterFlow e-commerce or booking app triggers a transactional receipt email after a Stripe payment succeeds. The action passes the order ID, total amount, and item list as `dynamic_template_data` to the Cloud Function, which renders those values into a polished receipt template in SendGrid and delivers it within seconds.

Prompt example:

```
After a user completes a purchase, I want them to receive an order confirmation email with the order number, item names, quantities, and total price. The email should match my app's branding colours and logo.
```

### In-app contact form that emails the founder directly

A simple contact or feedback screen in a FlutterFlow app collects a name, email, and message from the user. On submit, a Cloud Function forwards those details to SendGrid using a notification template that lands in the founder's inbox within seconds. The founder can reply directly to the user's email address using SendGrid's reply-to field.

Prompt example:

```
I want a contact form screen where users type their name, email, and message. When they tap Send, I want to receive their message in my inbox immediately. The reply-to should be set to the user's email address so I can reply directly.
```

## Troubleshooting

### Cloud Function returns 403 or email contains 'The from address does not match a verified Sender Identity'

Cause: The `from.email` field in your Cloud Function's request payload does not match any verified sender address or domain in SendGrid.

Solution: Log in to SendGrid → Settings → Sender Authentication and confirm the exact email address or domain you verified. Update the `from.email` value in the Cloud Function source code to match exactly, including capitalisation. Redeploy the function and retry.

### API Call test in FlutterFlow returns 200 but the email never arrives

Cause: You may have exceeded the SendGrid free-tier daily cap of 100 emails, or the email is being delivered to the recipient's spam folder, or a suppression (bounce or unsubscribe) is blocking delivery.

Solution: Check SendGrid's Activity Feed (Email API → Activity → Activity Feed) and look for the email. The feed shows whether it was delivered, bounced, or suppressed. If suppressed, go to Settings → Suppressions and remove the address. If you have exceeded the daily cap, the Activity Feed will show 'drop' status with a 'daily limit exceeded' reason — upgrade your SendGrid plan.

### FlutterFlow API Call returns an XMLHttpRequest error when testing in web Run Mode

Cause: The Cloud Function is missing CORS headers, so the browser blocks the request when FlutterFlow's web preview tries to call it.

Solution: Ensure your Cloud Function sets `Access-Control-Allow-Origin: *` (or your specific domain) and handles the OPTIONS preflight request before the POST logic. The code snippet in Step 2 includes the correct CORS headers — verify they are present and that the function has been redeployed after any changes.

### Email arrives but Dynamic Template variables render as blank or literal {{handlebars}} tokens

Cause: The keys in the `dynamic_template_data` object sent from FlutterFlow do not match the Handlebars token names in the SendGrid template design.

Solution: Open the Dynamic Template in SendGrid → Email API → Dynamic Templates, click the version, and check the Preview tab with test data. The Handlebars token `{{name}}` requires a key named exactly `name` in `dynamic_template_data`. Update either the template tokens or the `dynamic_data` JSON object in your FlutterFlow action to ensure the key names match exactly.

## Frequently asked questions

### Does FlutterFlow have a native SendGrid integration like Retool does?

No. Retool ships a native SendGrid connector that you configure through the Retool UI without writing code. FlutterFlow has no such native integration — you build it using the FlutterFlow API Call method with a Firebase Cloud Function proxy as described on this page. The proxy pattern is slightly more setup but gives you full control over the email payload and keeps the API key secure.

### Can I send emails directly from the FlutterFlow API Call without a Cloud Function?

Technically yes — you can add a FlutterFlow API Call that POSTs directly to api.sendgrid.com with the API key in the Authorization header. However, this means the key ships inside your app binary and can be extracted by anyone who downloads your app. Given that a SendGrid API key can be used to send email as your verified domain (which damages your sender reputation) or to access your contact lists, this is a serious security risk and should never be done in production.

### Why does SendGrid return 202 instead of 200 on success?

HTTP 202 Accepted means SendGrid has received and queued the email for delivery, not that it has been delivered. Delivery happens asynchronously and typically completes within seconds, but the 202 response arrives before delivery is confirmed. This is standard REST API design for asynchronous operations. In your Cloud Function, treat 202 as success and return a 200 to FlutterFlow, since FlutterFlow's API Call response handling is simpler when working with 200 status codes.

### How do I send to multiple recipients from FlutterFlow?

To send to multiple recipients, your Cloud Function needs to accept an array of email addresses and build a `personalizations` array with multiple `to` entries, or add recipients to `cc` or `bcc`. Pass a comma-separated string or a JSON array from FlutterFlow and split it in the Cloud Function. For bulk sending to marketing lists, use SendGrid's Marketing Campaigns product rather than the transactional API, as it handles unsubscribes, list management, and CAN-SPAM compliance automatically.

### What if I'd rather not manage a Firebase Cloud Function?

If setting up and maintaining a Cloud Function feels like too much, RapidDev's team builds FlutterFlow integrations like this regularly — including the Cloud Function deployment and FlutterFlow action wiring — with a free scoping call available at rapidevelopers.com/contact. A Supabase Edge Function is an alternative proxy runtime if you are already using Supabase in your project, as FlutterFlow has a native Supabase integration that makes Edge Function calls straightforward to configure.

---

Source: https://www.rapidevelopers.com/flutterflow-integrations/sendgrid
© RapidDev — https://www.rapidevelopers.com/flutterflow-integrations/sendgrid
