# SES API

- Tool: FlutterFlow
- Difficulty: Advanced
- Time required: 60 minutes
- Last updated: July 2026

## TL;DR

Connect FlutterFlow to Amazon SES via a Firebase Cloud Function that holds your AWS IAM credentials and handles SigV4 request signing. FlutterFlow sends a simple JSON payload to your Cloud Function — never to SES directly — because AWS Signature Version 4 signing requires a Secret Access Key that cannot ship inside the compiled app. New accounts must also exit the SES sandbox before sending to arbitrary recipients.

## Amazon SES + FlutterFlow: Cheap Transactional Email via a Cloud Function Proxy

Amazon SES is the most cost-effective transactional email API at scale — $0.10 per 1,000 emails with no monthly minimum. For FlutterFlow apps that need to send high volumes of transactional email (order confirmations, notifications, password resets), SES is compelling on price. But SES is also the email API with the most friction for FlutterFlow builders, for two reasons.

First, every SES API request must be signed with **AWS Signature Version 4** (SigV4) — a cryptographic signing process that uses your IAM Secret Access Key. This is not a header you can paste into FlutterFlow's API Calls panel. The signing process involves computing HMAC hashes over the request headers, path, query string, and body in a specific order. The AWS SDK handles this automatically, but the SDK runs in Node.js or Python — not in Flutter/Dart on the user's device. Even if you wrote SigV4 signing in Dart, the IAM Secret Access Key would ship inside your compiled app and be decompilable. The correct and only viable architecture is a Cloud Function that holds the IAM key and uses the AWS SDK to sign and send.

Second, all new SES accounts start in **sandbox mode**, which means you can only send to email addresses you've individually verified. Before launching your app to real users, you must submit a Production Access request in the SES console. This approval can take 24–48 hours. The SES endpoint URL is also region-specific (`email.us-east-1.amazonaws.com`, `email.eu-west-1.amazonaws.com`, etc.) — verified sender identities in one region are not valid in another.

## Before you start

- An AWS account at aws.amazon.com — SES is available on all AWS accounts; new accounts start in sandbox mode
- A domain you own (to verify as a sending identity in SES for best deliverability) — or at minimum a single email address you can verify
- A Firebase project with Cloud Functions on the Blaze pay-as-you-go plan (required for external HTTP calls to AWS), OR a Supabase project with Edge Functions
- A FlutterFlow project — API Calls are available on all paid FlutterFlow plans
- Basic familiarity with the AWS IAM console and Firebase Functions deployment

## Step-by-step guide

### 1. Verify your sender identity in SES and request production access

Log in to the AWS Console at console.aws.amazon.com and navigate to **Amazon SES** (search 'SES' in the top search bar). First, select the AWS **region** you want to use — this matters because sending identities (email addresses and domains) are region-specific and your endpoint URL must match. Common choices are us-east-1 (N. Virginia) or eu-west-1 (Ireland).

Click **'Verified identities'** in the left nav → **'Create identity'**. Choose either 'Email address' (simplest for testing — you'll receive a confirmation email to click) or 'Domain' (recommended for production — follow the DNS verification wizard to add TXT, DKIM CNAME records to your domain's DNS). For domain verification, SES will also offer to configure DKIM for you automatically via Route 53 or give you the CNAME records to add manually.

Once your identity is verified (shows green 'Verified' status), note that your account is still in **sandbox mode**. In sandbox, you can only send to verified email addresses. To send to any recipient, you must request production access: click **'Account dashboard'** in the left nav → **'Request production access'**. Fill out the form describing your use case, expected sending volume, and how you handle unsubscribes/bounces. AWS typically responds within 24 hours. Production access is usually granted unless your described use case violates SES policy (no bulk unsolicited email).

**Expected result:** Your sender domain (or email address) shows 'Verified' in SES. You've submitted (or already received) a production access request. You know your chosen AWS region.

### 2. Create an IAM user with minimal SES permissions

In the AWS Console, navigate to **IAM** (search 'IAM'). Click **'Users'** → **'Create user'**. Name it something like `flutterflow-ses-sender`. On the permissions step, choose **'Attach policies directly'** → click **'Create policy'**. Switch to the JSON editor and paste the minimal policy below — it grants only `ses:SendEmail` and `ses:SendRawEmail` on all SES resources in your account. This is the principle of least privilege: if this key is ever compromised, it can only be used to send email, not to manage other AWS resources.

After creating the policy, attach it to the user and finish creating the user. On the final confirmation page, click **'Create access key'** → choose **'Application running outside AWS'**. AWS will show you the **Access Key ID** and **Secret Access Key** exactly once. Copy both immediately and store them in a password manager — the Secret Access Key cannot be retrieved again.

Never paste these credentials into FlutterFlow. In the next step, you'll store them in Firebase Functions environment config, where they'll be available to the Cloud Function at runtime without being exposed.

```
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "ses:SendEmail",
        "ses:SendRawEmail"
      ],
      "Resource": "*"
    }
  ]
}
```

**Expected result:** An IAM user exists with a policy granting only ses:SendEmail and ses:SendRawEmail. You have the Access Key ID and Secret Access Key saved securely.

### 3. Deploy a Firebase Cloud Function as the SES proxy

Open your Firebase project and create a new Cloud Function named `sendEmail`. This function uses the AWS SDK for JavaScript (`@aws-sdk/client-ses`) to sign requests with SigV4 automatically and call the SES v2 SendEmail API. The function accepts a JSON POST from FlutterFlow with `{to, subject, html}` and maps it to the SES SDK call.

Add the AWS SDK to your Cloud Function dependencies: in `functions/package.json`, add `"@aws-sdk/client-ses": "^3.0.0"`. The AWS SDK v3 automatically handles SigV4 signing when you provide credentials — you don't write any signing code yourself.

Store your IAM credentials in Firebase Functions config before deploying:
```
firebase functions:config:set aws.access_key_id='AKIAIOSFODNN7EXAMPLE' aws.secret_access_key='wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY' aws.region='us-east-1' aws.from_email='hello@yourapp.com'
```

After deploying, copy the function URL from the Firebase console. Also add CORS headers to the function response so FlutterFlow's web build can call it from the browser.

```
// index.js (Firebase Cloud Function)
const functions = require('firebase-functions');
const { SESClient, SendEmailCommand } = require('@aws-sdk/client-ses');

const config = functions.config();
const sesClient = new SESClient({
  region: config.aws.region || 'us-east-1',
  credentials: {
    accessKeyId: config.aws.access_key_id,
    secretAccessKey: config.aws.secret_access_key
  }
});

exports.sendEmail = functions.https.onRequest(async (req, res) => {
  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('');
  }

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

  const command = new SendEmailCommand({
    Source: config.aws.from_email,
    Destination: { ToAddresses: [to] },
    Message: {
      Subject: { Data: subject, Charset: 'UTF-8' },
      Body: {
        ...(html ? { Html: { Data: html, Charset: 'UTF-8' } } : {}),
        ...(text ? { Text: { Data: text, Charset: 'UTF-8' } } : {})
      }
    }
  });

  try {
    const result = await sesClient.send(command);
    return res.status(200).json({ messageId: result.MessageId });
  } catch (err) {
    const code = err.name === 'MessageRejected' ? 400
      : err.name === 'SendingPausedException' ? 503
      : err.$metadata?.httpStatusCode || 500;
    return res.status(code).json({ error: err.name, message: err.message });
  }
});
```

**Expected result:** The Cloud Function is deployed and accessible at its HTTPS URL. Testing it with a POST containing {to, subject, html} sends an email via SES and returns a 200 with a MessageId.

### 4. Create the FlutterFlow API Group pointing at your Cloud Function

In FlutterFlow, click **API Calls** in the left nav, then **+ Add** → **Create API Group**. Name it 'SES Proxy'. Set the Base URL to your Cloud Function URL (e.g., `https://us-central1-your-project.cloudfunctions.net`). No authentication headers are needed on the FlutterFlow side — the Cloud Function handles all AWS credentials internally.

Click **+ Add API Call** inside the group, name it 'Send Email'. Set method to **POST**, path to `/sendEmail`. Set body type to **JSON** and add three body fields: `to`, `subject`, and `html` (all mapped to variables of the same names in the Variables tab).

Click **Test** and enter a verified SES recipient email in the `to` field (any email you've verified in SES if still in sandbox, or any email once production access is granted). If the function returns a 200 with a `messageId`, check your inbox. If you get an error, check the Firebase Functions logs — SES error messages like `MessageRejected` ('Email address not verified'), `AccountThrottled`, or `SendingPausedException` are surfaced clearly in the logs.

```
{
  "group": "SES Proxy",
  "baseUrl": "https://us-central1-YOUR-PROJECT.cloudfunctions.net",
  "calls": [
    {
      "name": "Send Email",
      "method": "POST",
      "path": "/sendEmail",
      "bodyType": "JSON",
      "body": {
        "to": "{{to}}",
        "subject": "{{subject}}",
        "html": "{{html}}"
      },
      "variables": [
        { "name": "to", "type": "String" },
        { "name": "subject", "type": "String" },
        { "name": "html", "type": "String" }
      ]
    }
  ]
}
```

**Expected result:** The 'Send Email' API Call is configured in FlutterFlow. Testing it delivers an email successfully and returns a messageId. The Cloud Function logs confirm SES received and accepted the request.

### 5. Wire the API Call to your app and handle SES-specific errors

Add a simple email compose screen to your FlutterFlow app: a TextField for the recipient email, a TextField for the subject, a multiline TextField for the message body, and a Send button. Wire the Send button's Action Flow to: (1) a Conditional Action that checks the email field is not empty and contains '@'; (2) if valid, an API Request action calling 'SES Proxy / Send Email', mapping the TextFields to `to`, `subject`, and `html` variables; (3) on response code 200, show a SnackBar 'Email sent!' and clear the form.

For error states, add handling for the three most common SES errors your Cloud Function will return:
- **400 (MessageRejected)**: the recipient or sender is not verified (sandbox mode) — show 'This email address is not authorized during testing mode. Contact support.'
- **503 (SendingPausedException)**: your SES account's sending has been paused by AWS (usually due to high bounce rates) — show 'Email sending temporarily unavailable. Please try again later.'
- **500**: unexpected error — show 'Failed to send. Please try again.'

If you need attachments, keep that logic in the Cloud Function: convert the attachment to a Buffer from a URL or base64 string, use `SendRawEmail` (via `SESv2Client.sendEmail` with a raw MIME body) instead of `SendEmail`. Pass the attachment URL or base64 from FlutterFlow to the Cloud Function and handle MIME assembly server-side — don't attempt MIME encoding in Dart.

For production apps handling lots of email sends, RapidDev's team sets up FlutterFlow email pipelines like this weekly — free scoping call at rapidevelopers.com/contact.

**Expected result:** The compose screen sends emails via SES and shows appropriate success or error messages. Sandbox-mode errors surface a readable message. The email arrives in the recipient's inbox within seconds.

## Best practices

- Never attempt to call SES directly from FlutterFlow or write SigV4 signing in Dart — the IAM Secret Access Key cannot safely live in the compiled app; always proxy through a Cloud Function
- Request production access from SES before launch — sandbox mode blocks all non-verified recipients and will silently prevent your users from receiving emails
- Use an IAM policy with minimal permissions (ses:SendEmail and ses:SendRawEmail only) — never use root AWS credentials or admin IAM keys in your Cloud Function
- Verify your sending domain (not just an email address) with DKIM and DMARC configured — domain verification improves deliverability significantly compared to email-address-only verification
- Set up SES Configuration Sets with SNS bounce and complaint notifications — AWS will pause your sending if bounce rates exceed 10%; monitoring these early prevents account suspension
- Store the SES MessageId returned by each send alongside your database record — it's your audit trail for proving delivery and debugging missing emails
- Match your sender identity region to your Cloud Function's AWS SDK region — SES identities verified in us-east-1 are invalid in eu-west-1, and region mismatches cause hard-to-debug MessageRejected errors

## Use cases

### Order confirmation emails for a FlutterFlow e-commerce app

A FlutterFlow marketplace app sends order confirmation emails at high volume. At $0.10 per 1,000 emails, SES costs a fraction of other providers. The checkout action triggers the Cloud Function with order details, which assembles an HTML receipt and sends it via SES from the verified business domain.

Prompt example:

```
After a purchase is completed, send the customer an HTML order confirmation email with itemized receipt and estimated delivery date.
```

### Password reset and auth notification emails for a custom auth system

A FlutterFlow app using a custom auth backend (not Firebase Auth) needs to send password reset links. The reset flow generates a secure token in the backend, then triggers the Cloud Function to send a reset email via SES — leveraging SES's high deliverability and domain reputation tools like DKIM and DMARC.

Prompt example:

```
When a user requests a password reset, send a reset link email from our verified domain using Amazon SES.
```

### Daily digest notifications for a productivity FlutterFlow app

A FlutterFlow task management app sends each user a daily summary email via SES — including their upcoming tasks, completed items, and streak count. At SES pricing, sending to thousands of users daily costs pennies. A Cloud Function triggered by a Firebase scheduled function assembles and sends each digest.

Prompt example:

```
Send each user a daily morning email summarizing their pending tasks and completed items from the previous day.
```

## Troubleshooting

### Cloud Function returns 400 with error 'MessageRejected' and message 'Email address is not verified'

Cause: Your SES account is in sandbox mode. You're either sending to an unverified recipient email address, or the sender address (your from_email config) is not verified in SES.

Solution: In sandbox mode, both the sender and recipient must be individually verified in SES. Go to SES → Verified identities → Create identity → enter the recipient's email address → they'll receive a click-to-verify email. For production, submit a production access request (SES → Account dashboard → Request production access) to remove this restriction. This is the #1 issue new SES users hit.

### Cloud Function receives the FlutterFlow request but SES returns 'InvalidClientTokenId' or 'SignatureDoesNotMatch'

Cause: The AWS Access Key ID or Secret Access Key in the Firebase Functions config is incorrect, has been deactivated, or the IAM user has been deleted.

Solution: Go to the AWS IAM console → Users → select your IAM user → Security credentials tab. Check if the access key is Active. If you're unsure whether the secret is correct (you can't retrieve it after creation), deactivate the old key and create a new one. Update the Firebase config: `firebase functions:config:set aws.access_key_id='NEW_KEY' aws.secret_access_key='NEW_SECRET'` and redeploy. Never copy-paste keys with hidden whitespace characters — they'll silently fail.

### Email sends succeed (200 response from Cloud Function) but emails never arrive in the inbox

Cause: Several possibilities: the email landed in spam, SES delivery was successful but the recipient's mail server rejected it silently, or there's a bounce/complaint that triggered SES to suppress the address.

Solution: Check the SES console under 'Sending statistics' for bounce and complaint rates. Check the recipient's spam folder. If the sender domain's DKIM or SPF records are not properly configured, many mail servers will mark SES emails as spam. Set up a Configuration Set in SES with an SNS topic for bounce notifications — this will alert you when deliveries fail and why. Also check if the recipient's address is on SES's suppression list (a previous bounce can add them).

### Cloud Function logs show 'Email address is not verified' even though the sender domain is verified

Cause: The from_email in Firebase config uses a subdomain or address that doesn't match the verified identity in SES. SES verifies identities at the domain level (mail.example.com) but if your from_email uses the root domain (example.com) or vice versa, there's a mismatch.

Solution: In SES → Verified identities, confirm what domain or email address is verified. If you verified `example.com` (domain), you can send from any `@example.com` address. If you verified `hello@example.com` (email), you can only send from that exact address. Update your `aws.from_email` config to match what's verified, and ensure the region in your Cloud Function matches the region where the identity is verified.

## Frequently asked questions

### How long does it take for Amazon to grant SES production access?

AWS typically responds within 24 hours, though some requests take up to 48 hours. The most common reason for denial is an unclear use case description — be specific about what emails you're sending (transactional only, not bulk marketing), your expected volume, and how you handle unsubscribes and bounces. If denied, you can revise and resubmit the request.

### Can I send attachments via SES from FlutterFlow?

Yes, but attachment handling must happen in the Cloud Function. FlutterFlow passes the attachment as a base64-encoded string or as a URL to a file in Firebase Storage. The Cloud Function then constructs a MIME email using the `SendRawEmail` API (with the full MIME body including headers, text/html parts, and the attachment). Never attempt MIME assembly in Dart inside FlutterFlow — it belongs in the Cloud Function where the full AWS SDK and standard Node.js libraries are available.

### Is Amazon SES only for sending, or can it receive email too?

SES can also receive email in supported regions (us-east-1, eu-west-1, us-west-2) via SES receiving rules that route inbound messages to S3, SNS, or Lambda. However, receiving email is a separate, more complex setup unrelated to this tutorial. For reading a user's inbox, use the Gmail API or Outlook 365 API (IMAP/SMTP protocols) instead — SES receiving is for routing email delivered to your domain's addresses, not for reading personal mailboxes.

### How much does Amazon SES cost in practice for a typical app?

At $0.10 per 1,000 emails, sending 50,000 emails/month costs $5. There's no monthly minimum — you pay only for what you send. Data transfer charges may apply for large attachments. For apps sending email from EC2 or other AWS services in the same region, the first 62,000 emails per month are free — but that free tier doesn't apply to calls from Firebase Cloud Functions (which run on Google Cloud). Always check current pricing at aws.amazon.com/ses/pricing.

### Can I use SES with a Gmail or Yahoo address as the sender?

No — SES requires you to verify that you own or control the sender domain. You cannot send from a Gmail, Yahoo, or other third-party email address through SES. This is both a technical constraint (SES won't let you verify addresses at domains you don't control) and a deliverability one (DMARC policies on gmail.com and yahoo.com actively block third-party sending). Use your own custom domain as the sender for all SES sends.

---

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