# AWS SES

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

## TL;DR

Connect FlutterFlow to AWS SES by routing all email sending through a Firebase Cloud Function or Supabase Edge Function — FlutterFlow POSTs the message payload (to, subject, body) to your Function, which holds the IAM credentials and calls the SES SendEmail API. AWS credentials never touch the device. Before going live, verify your sender domain and request production access to leave the SES sandbox.

## Why AWS SES Needs a Server-Side Proxy in FlutterFlow

AWS SES sends transactional email — order confirmations, password resets, account verifications — reliably and cheaply. Pricing starts at approximately $0.10 per 1,000 emails, with around 3,000 free emails per month when sending from an AWS-hosted application (check AWS's current pricing page for the latest figures). For founders building FlutterFlow apps that need professional email delivery without the cost and limitations of third-party services, SES is an excellent choice.

The challenge is security. Every SES API call must be signed with an IAM access key and secret using the SigV4 algorithm. FlutterFlow compiles to Flutter code that runs on your users' devices — iOS phones, Android devices, browsers. If you put the IAM credentials anywhere in FlutterFlow (API Call headers, App Values, Dart custom actions), those credentials ship inside the compiled app binary. Anyone with the right tools can extract them and use your SES account to send spam at your expense. This is not a theoretical risk — it happens.

The solution is a one-step indirection: a Firebase Cloud Function (or Supabase Edge Function) acts as the secure middleman. The Function holds the IAM credentials in its environment config, signs the SES request using the AWS SDK v3, and sends the email. Your FlutterFlow app only ever calls the Function URL with a harmless JSON payload: the recipient address, subject, and body. No AWS credentials ever leave your server. Because the Function is a standard HTTPS endpoint, this pattern works identically on iOS, Android, and web — no CORS issues, no socket limitations.

## Before you start

- An AWS account (free tier works to start)
- A FlutterFlow project with at least one form or trigger point (signup, purchase, etc.)
- A Firebase project with Cloud Functions enabled (Blaze plan required for outbound HTTP calls), or a Supabase project with Edge Functions
- A domain you control for sender verification in SES (or at minimum an email address you can verify)
- Basic familiarity with the FlutterFlow Action Flow Editor

## Step-by-step guide

### 1. Verify Your Sender Domain (or Email) in AWS SES

Before SES can deliver a single email, it needs to confirm you own the sending address or domain. Log into the AWS Console, navigate to Amazon SES, and open the 'Verified identities' page. Click 'Create identity' and choose either 'Domain' or 'Email address'. If you choose Domain (recommended for production), AWS will show you a set of CNAME and TXT DNS records to add to your domain registrar — these implement DKIM signing, which dramatically improves deliverability and prevents your emails from landing in spam. Log into your DNS provider (Cloudflare, Namecheap, Route 53, etc.) and add those records exactly as shown. Verification usually propagates within 30–60 minutes; you will see the status change to 'Verified' in the SES console.

If you only have a Gmail or other free email address to test with, you can verify a single email address instead — SES will send you a verification link to click. Keep in mind that single-address verification is for testing only; production apps should use a domain identity so that the 'From' address matches your brand and passes email authentication checks. Choose a region for SES (for example, us-east-1) — make a note of it because every API call must target this same region. If you try to send from a different region than where you verified the identity, SES will reject the request.

**Expected result:** The SES Verified identities page shows your domain or email with a green 'Verified' badge.

### 2. Request Production Access to Leave the SES Sandbox

Every brand-new AWS SES account starts in the 'sandbox' — a restricted mode designed to prevent abuse. In the sandbox you can only send emails to addresses you have individually verified, and you are limited to 200 emails per day with a maximum sending rate of 1 per second. For a real app where users sign up with their own email addresses (which you have not pre-verified), the sandbox is unusable. You must request 'production access' before launching.

In the AWS SES console, open the account dashboard and look for the 'Request production access' button (sometimes labeled 'Move out of sandbox'). Fill out the support request form: describe your use case (transactional emails like order confirmations or verifications), your expected daily volume, and how you handle bounces and complaints. AWS typically reviews these requests within 24–48 hours. While you wait, you can continue building — just use verified test addresses for all your development sends. Do not skip this step and launch with sandbox mode active; your users will never receive their emails, and you will see confusing 'MessageRejected' errors that look like code problems but are actually account restrictions. This is one of the two most common footguns for founders new to SES.

**Expected result:** The SES account dashboard shows 'Production access' with your sending limit (e.g., 50,000 emails/day), no longer showing 'Sandbox'.

### 3. Create an IAM User and Write a Cloud Function That Sends Email

In the AWS IAM console, create a new user with programmatic access only (no console login needed). Attach a policy that grants exactly `ses:SendEmail` and `ses:SendRawEmail` on the specific SES region resource — nothing else. Download the Access Key ID and Secret Access Key; you will only be shown the secret once. These credentials stay on the server — never paste them into FlutterFlow.

Now write the Cloud Function. In your Firebase project, open the Cloud Functions folder (or create one with `firebase init functions` locally if you have the Pro FlutterFlow plan for code export). Create an HTTPS-callable Function named something like `sendEmail`. Install the AWS SDK v3 package: `@aws-sdk/client-ses`. The Function reads the IAM credentials from environment config (set via `firebase functions:config:set` or Firebase Secret Manager — NOT hardcoded), constructs an `SESv2Client`, and calls `SendEmailCommand`. The function accepts a JSON body with `to`, `subject`, and `html` fields, validates them, then sends the email. Store the IAM key and secret in Firebase Functions config or Secret Manager, never in the Function source code. Deploy with `firebase deploy --only functions`. If you are using Supabase Edge Functions instead, the pattern is identical but written in Deno TypeScript, with the IAM credentials stored as Supabase secrets.

```
// Firebase Cloud Function (Node.js) — functions/index.js
const { onRequest } = require('firebase-functions/v2/https');
const { SESv2Client, SendEmailCommand } = require('@aws-sdk/client-ses');

exports.sendEmail = onRequest({ cors: true }, async (req, res) => {
  if (req.method !== 'POST') {
    return res.status(405).send('Method Not Allowed');
  }

  const { to, subject, html } = req.body;
  if (!to || !subject || !html) {
    return res.status(400).json({ error: 'Missing required fields: to, subject, html' });
  }

  const client = new SESv2Client({
    region: process.env.AWS_REGION, // e.g. 'us-east-1'
    credentials: {
      accessKeyId: process.env.AWS_ACCESS_KEY_ID,
      secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
    },
  });

  const command = new SendEmailCommand({
    FromEmailAddress: process.env.SES_FROM_ADDRESS, // verified identity
    Destination: { ToAddresses: [to] },
    Content: {
      Simple: {
        Subject: { Data: subject, Charset: 'UTF-8' },
        Body: { Html: { Data: html, Charset: 'UTF-8' } },
      },
    },
  });

  try {
    await client.send(command);
    return res.status(200).json({ success: true });
  } catch (err) {
    console.error('SES error:', err);
    return res.status(500).json({ error: err.message });
  }
});
```

**Expected result:** The Cloud Function deploys successfully and you can test it with a curl POST to the Function URL, receiving a 200 response and seeing the email arrive in your inbox.

### 4. Build a FlutterFlow API Group Pointing to Your Cloud Function

Now connect FlutterFlow to your deployed Cloud Function. In FlutterFlow, click 'API Calls' in the left navigation panel, then click '+ Add' and select 'Create API Group'. Name it something like 'EmailService'. Set the Base URL to your Cloud Function's HTTPS URL — it looks like `https://us-central1-your-project.cloudfunctions.net` (copy this from the Firebase console under Functions). You do not need any shared authentication headers here because the Cloud Function itself handles AWS auth — your Function can optionally validate a simple bearer token or secret header you set if you want to restrict who can call it.

Inside the group, click '+ Add API Call' and create a call named 'SendEmail'. Set the method to POST and the endpoint to `/sendEmail` (matching your function name). Switch to the 'Body' tab and set the body type to JSON. Add three body parameters: `to` (String), `subject` (String), and `html` (String). Each should be defined as a variable so you can pass dynamic values from your FlutterFlow widgets — name them `{{ to }}`, `{{ subject }}`, and `{{ html }}` in the body template. Switch to the 'Response & Test' tab, paste a sample success response like `{ "success": true }`, and click 'Generate JSON Paths' so FlutterFlow can parse the response. Click 'Test API Call', fill in your test values, and you should get a 200 response. The email should arrive in your inbox within seconds.

```
{
  "method": "POST",
  "base_url": "https://us-central1-YOUR_PROJECT.cloudfunctions.net",
  "endpoint": "/sendEmail",
  "headers": {
    "Content-Type": "application/json"
  },
  "body": {
    "to": "{{ to }}",
    "subject": "{{ subject }}",
    "html": "{{ html }}"
  }
}
```

**Expected result:** The API Call test returns a 200 response and you see 'SendEmail' listed under the EmailService group in the API Calls panel.

### 5. Trigger the Email Action from a Form or Event in Action Flow Editor

The last step is wiring the API call to a real user event. Navigate to the widget that should trigger the email — for example, a 'Submit' button on a registration form or a purchase confirmation button. Open its Actions panel by clicking the lightning bolt icon, then click '+ Add Action'. Choose 'Backend/API Call' from the action types, select your 'EmailService' group and the 'SendEmail' call. Now map the variables: for the `to` field, bind it to the email TextField's value (or the logged-in user's email from Firebase Auth using the 'Authenticated User' data source). For `subject`, you can type a static string like 'Welcome to [AppName]!' or make it dynamic. For `html`, bind it to a string variable you compose earlier in the Action Flow using a 'Set Variable' action — build the HTML body as a string with the user's name interpolated.

For more complex flows — like a multi-step order confirmation — consider adding a 'Show Snack Bar' or 'Navigate' action AFTER the API call to give the user immediate feedback, and add a second action branch on API failure (check the 'On Error' tab in the Action chain) to show an error message. Once configured, run the app in FlutterFlow's Test mode, submit the form, and check your inbox. Keep in mind that custom API calls work in Test mode as well as on device — you should see the email arrive within a few seconds.

**Expected result:** Submitting the form in Test mode triggers the API call, returns a success response, and the recipient receives the email in their inbox within seconds.

## Best practices

- Never store IAM credentials in FlutterFlow App Values, API Call headers, or Dart custom actions — they belong exclusively in Cloud Function environment config or Firebase Secret Manager.
- Use the principle of least privilege: create a dedicated IAM user with only ses:SendEmail and ses:SendRawEmail permissions, scoped to the specific SES ARN for your verified region.
- Request SES production access before your beta launch — AWS review takes 24–48 hours, and sandbox restrictions will silently block all emails to real users.
- Verify your sender domain (not just an email address) and configure both DKIM CNAME records and an SPF TXT record to maximize deliverability and avoid spam filters.
- Add bounce and complaint handling: configure SES to route bounce/complaint notifications to an SNS topic, then process them in a Cloud Function to mark bad addresses in your database and suppress future sends.
- Send emails from a subdomain (e.g., mail.yourdomain.com) rather than your root domain — this protects your root domain's reputation if a batch send generates complaints.
- Log the SES message ID returned by each successful SendEmailCommand — store it against the user record in Firestore so you can trace deliverability issues later.
- Test your email templates in multiple clients (Gmail, Outlook, Apple Mail) using a tool like Litmus or Email on Acid before going live — HTML email rendering varies widely across clients.

## Use cases

### SaaS app that sends email verification on signup

After a user registers in your FlutterFlow app, an Action Flow triggers a POST to your Cloud Function with the user's email and a verification link. The Function sends a branded HTML email via SES, letting users confirm their address before accessing the app.

Prompt example:

```
Build a signup screen that, after the user submits their email and password, calls a Cloud Function to send a verification email using AWS SES with a tokenized confirmation link.
```

### E-commerce app that emails order receipts

When a customer completes a purchase in your FlutterFlow app, the order-confirmation Action Flow POSTs the order details to your Cloud Function, which assembles and sends a formatted HTML receipt via SES — complete with item list, total, and shipping details.

Prompt example:

```
After an order is placed, trigger a POST to a Cloud Function endpoint that formats the order data and sends a receipt email through AWS SES to the customer's address stored in the user record.
```

### Password reset flow for a custom auth system

For apps using a custom authentication flow (not Firebase Auth), the 'Forgot Password' button triggers a POST to your Cloud Function with the user's email. The Function generates a reset token, stores it in Firestore, and sends the reset link via SES.

Prompt example:

```
Create a Forgot Password screen that posts the entered email to a Cloud Function; the Function looks up the user, generates a password reset token, and sends a reset email via AWS SES.
```

## Troubleshooting

### Email is rejected with 'MessageRejected: Email address is not verified' error in Function logs

Cause: The SES account is still in sandbox mode, or the recipient's email address has not been individually verified as a test identity.

Solution: Either request production access (SES console → Request production access) to lift the sandbox restriction, or navigate to SES → Verified identities and verify the specific recipient email address you are testing with. In production, sandbox mode must be removed — there is no other option.

### Emails are sent but arrive in spam or are rejected by the receiving mail server

Cause: The sender domain is missing DKIM CNAME records or SPF TXT record, causing the receiving server to treat the email as unauthenticated.

Solution: Return to SES → Verified identities → your domain and check the DKIM status. If it shows 'Pending', the DNS records have not propagated yet or were entered incorrectly. Verify the CNAME records at your registrar exactly match what SES shows. Also add an SPF TXT record: `v=spf1 include:amazonses.com ~all`. Allow up to 72 hours for DNS propagation.

### Cloud Function returns 500 error with 'InvalidClientTokenId' or 'AuthFailure' in logs

Cause: The IAM Access Key ID or Secret Access Key stored in the Function environment config is incorrect, has been deactivated, or the region in the SESv2Client config does not match the region where the SES identity was verified.

Solution: Check the Function environment variables (Firebase console → Functions → your function → Configuration) and confirm the key and secret match what you downloaded from IAM. Verify the `AWS_REGION` variable matches the SES region exactly (e.g., `us-east-1` not `us-east1`). If the keys were rotated or deleted in IAM, create new ones and update the Function config, then redeploy.

### FlutterFlow API Call test returns a CORS error when testing in the browser

Cause: The Cloud Function was deployed without CORS headers, or the FlutterFlow test runner is making a preflight OPTIONS request that the Function is not handling.

Solution: Ensure your Cloud Function includes `{ cors: true }` in the `onRequest` options (Firebase Functions v2) or manually sets the `Access-Control-Allow-Origin` header. The code example in Step 3 already includes this. If using Supabase Edge Functions, add `corsHeaders` to every response. Redeploy the Function after the fix.

```
// Add cors option to onRequest (Firebase Functions v2)
exports.sendEmail = onRequest({ cors: true }, async (req, res) => { ... });
```

## Frequently asked questions

### Can I call AWS SES directly from FlutterFlow without a Cloud Function?

No — and you should not try. SES requires SigV4 request signing using your IAM secret key, and there is no presigned-URL workaround like S3 offers. Placing those credentials in FlutterFlow means they ship inside your compiled app binary, where anyone can extract them and send email (and rack up bills) on your behalf. The Cloud Function proxy is a one-time 45-minute setup and is the only safe architecture.

### Why are my emails going to spam even though SES says they were accepted?

SES accepted the message for delivery, but the receiving mail server decided it was suspicious. The most common causes are missing DKIM records (verify CNAME records in SES → your domain identity), missing SPF record, or a 'From' address on a domain with no email history. Add DKIM + SPF, send from a subdomain, and avoid common spam trigger words in subject lines. Check your SES account reputation dashboard for bounce and complaint rates.

### How do I handle email bounces so I stop sending to bad addresses?

Configure an SES notification: in the SES console, go to your verified identity → Notifications → Bounces and set up an SNS topic. Create a Cloud Function subscribed to that SNS topic that, when it receives a bounce notification, marks the email address as invalid in your Firestore database. Before sending any email, check that flag and skip sending if the address has bounced. This keeps your sender reputation score healthy and avoids SES suspending your account for high bounce rates.

### My FlutterFlow app is in development — do I need production SES access to test?

No. During development you can stay in the SES sandbox and verify specific test email addresses (including your own Gmail) as identities. Emails to those verified addresses will work fine. Just remember to request production access well before your public launch, since the review takes time. You can also use SES's 'simulator' addresses (like success@simulator.amazonses.com) to test various scenarios without consuming your sandbox quota.

### If you'd rather skip managing Cloud Functions yourself, what's the alternative?

RapidDev's team handles FlutterFlow integrations like AWS SES every week, including Cloud Function setup, IAM configuration, and domain verification. A free scoping call is available at rapidevelopers.com/contact if you'd prefer an expert to set this up for you.

---

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