# Mailgun

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

## TL;DR

Connect FlutterFlow to Mailgun by building a Firebase Cloud Function that sends email via Mailgun's form-urlencoded API, then calling that function from a FlutterFlow API Call. Mailgun authenticates with HTTP Basic Auth using the literal username 'api' and your Private API Key — credentials that cannot safely live in a compiled Flutter app and must stay server-side.

## Send Transactional Email from Your FlutterFlow App with Mailgun

Every production app needs to send transactional emails — welcome messages when users sign up, receipts after purchases, password reset links, and shipping confirmations. Mailgun is built exactly for this: a developer-focused email API with high deliverability, detailed logs, and a pay-as-you-go pricing model starting at around $15/month for the Flex plan (verify current pricing in your Mailgun dashboard). The free trial tier lets you send to verified addresses for testing.

FlutterFlow adds two important constraints to this integration. First, Mailgun authenticates with HTTP Basic Auth using the username `api` (literal — not your email address) and your Private API Key as the password. Embedding that key in a FlutterFlow API Call ships it inside the compiled Flutter app bundle, where it can be extracted with basic tooling from any device that has the app installed. The only safe pattern is a Firebase Cloud Function or Supabase Edge Function that holds the key server-side and accepts simple message-parameter requests from FlutterFlow. Second, Mailgun's send endpoint requires `application/x-www-form-urlencoded` body encoding — not JSON — and this is a consistent source of 400 errors when developers assume JSON.

Mailgun also has a hard split between US and EU infrastructure. US-hosted accounts use `api.mailgun.net` and EU-hosted accounts use `api.eu.mailgun.net`. Using the wrong regional base URL produces authentication success but silent send failures or empty responses — the most common 'why isn't Mailgun sending?' mystery. Your sending domain is set up in the Mailgun dashboard under Sending → Domains, and the region is shown there. This page walks through the Cloud Function proxy pattern, then connects it to FlutterFlow with a clean API Call.

## Before you start

- A Mailgun account with a verified sending domain and the Private API Key copied from Mailgun's API Security settings
- Your Mailgun account region noted (US: api.mailgun.net / EU: api.eu.mailgun.net)
- A Firebase project with Cloud Functions enabled (Blaze pay-as-you-go plan required for external network calls)
- A FlutterFlow project connected to Firebase or using Supabase as the backend
- Node.js installed locally for Cloud Function development (or use the Firebase Console inline editor)

## Step-by-step guide

### 1. Verify Your Sending Domain and Copy the Private API Key

Log into the Mailgun dashboard and navigate to Sending → Domains. If you haven't already, add your sending domain (e.g., `mail.yourdomain.com`) and complete the DNS verification steps by adding the CNAME and TXT records to your domain registrar. Verification can take up to 48 hours but usually completes within an hour. Once verified, the domain status shows a green checkmark. Next, go to Settings (the gear icon, top right) → API Security → API Keys. Copy your **Private API Key** — it starts with `key-` followed by a long alphanumeric string. This is the credential that authenticates every send request. Keep it in a secure location like a password manager. While you're here, note which region your account is on: US accounts are on `api.mailgun.net` and EU accounts are on `api.eu.mailgun.net`. The region setting is shown on the Domains page. Using the wrong regional URL is one of the most common silent failure points — you'll authenticate successfully but Mailgun will return empty results or the email simply won't send. Also check that the 'From' email address you plan to use is authorized under your verified domain (e.g., `noreply@mail.yourdomain.com`).

**Expected result:** You have a verified sending domain, a Private API Key starting with `key-`, and have confirmed your account region (US or EU).

### 2. Write and Deploy a Firebase Cloud Function that Sends via Mailgun

Your Firebase Cloud Function is the secure proxy that holds the Mailgun Private API Key and makes the actual send request. In your Firebase project, navigate to Functions in the Firebase Console and click 'Get started', or if you already have a local functions directory from `firebase init`, open `functions/index.js`. Install the `node-fetch` package for HTTP requests and add the function below. The function receives a POST request from FlutterFlow with the `to`, `subject`, `text`, and optional `html` fields in the JSON body. It then makes a form-urlencoded POST to Mailgun on your behalf. Store your Mailgun Private API Key and sending domain in Firebase Functions config using the Firebase CLI: run `firebase functions:config:set mailgun.key="key-yourkey" mailgun.domain="mail.yourdomain.com" mailgun.region="us"` in your terminal, then deploy with `firebase deploy --only functions`. Note that the Blaze (pay-as-you-go) plan is required for Cloud Functions to make outbound network calls — the free Spark plan blocks external HTTP requests. Set a budget alert in the Firebase console to avoid surprise bills.

```
// functions/index.js — Mailgun send proxy
const functions = require('firebase-functions');
const fetch = require('node-fetch');
const FormData = require('form-data');

const MAILGUN_KEY = functions.config().mailgun.key;
const MAILGUN_DOMAIN = functions.config().mailgun.domain;
const MAILGUN_REGION = functions.config().mailgun.region || 'us';
const MAILGUN_BASE =
  MAILGUN_REGION === 'eu'
    ? 'https://api.eu.mailgun.net/v3'
    : 'https://api.mailgun.net/v3';

exports.sendEmail = functions.https.onRequest(async (req, res) => {
  res.set('Access-Control-Allow-Origin', '*');
  if (req.method === 'OPTIONS') { res.status(204).send(''); return; }
  if (req.method !== 'POST') { res.status(405).json({ error: 'POST only' }); return; }

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

  const form = new FormData();
  form.append('from', from || `noreply@${MAILGUN_DOMAIN}`);
  form.append('to', to);
  form.append('subject', subject);
  if (text) form.append('text', text);
  if (html) form.append('html', html);

  try {
    const mgRes = await fetch(`${MAILGUN_BASE}/${MAILGUN_DOMAIN}/messages`, {
      method: 'POST',
      headers: {
        Authorization: 'Basic ' + Buffer.from(`api:${MAILGUN_KEY}`).toString('base64'),
      },
      body: form,
    });
    const data = await mgRes.json();
    if (!mgRes.ok) {
      return res.status(mgRes.status).json({ error: data.message || 'Mailgun error' });
    }
    res.status(200).json({ success: true, id: data.id });
  } catch (err) {
    res.status(500).json({ error: err.message });
  }
});
```

**Expected result:** The Cloud Function deploys successfully. The Firebase Console shows the function URL (e.g., `https://us-central1-your-project.cloudfunctions.net/sendEmail`). A test POST to that URL triggers an email delivery visible in the Mailgun logs.

### 3. Add the Mailgun API Group and Cloud Function API Call in FlutterFlow

In your FlutterFlow project, click 'API Calls' in the left navigation panel. Click '+ Add' and select 'Create API Group'. Name the group 'MailgunEmail'. For the base URL, enter your Firebase Cloud Function base URL: `https://us-central1-your-project.cloudfunctions.net`. In the Headers section, add `Content-Type: application/json` — your FlutterFlow call is sending JSON to your Cloud Function (the function handles the form-urlencoded conversion to Mailgun internally). Now add an API Call inside the group: click '+ Add API Call', name it 'sendEmail', set the method to POST, and enter `/sendEmail` as the endpoint path (matching your function name). Open the Variables tab and add: `to` (String, required), `subject` (String, required), `text` (String, required), and optionally `html` (String) and `from` (String). In the Body tab, set the type to JSON and build the body using your variables: `{ "to": "{{ to }}", "subject": "{{ subject }}", "text": "{{ text }}" }`. Go to Response & Test, paste a sample response like `{ "success": true, "id": "<20240101.message-id@domain>" }`, and generate JSON Paths. Click 'Test API Call' with real values to verify an email lands in your inbox. Once confirmed, save and close.

```
{
  "to": "{{ to }}",
  "subject": "{{ subject }}",
  "text": "{{ text }}",
  "html": "{{ html }}",
  "from": "{{ from }}"
}
```

**Expected result:** The API Call configuration is saved and a test sends a real email to your test address. The Mailgun dashboard logs show the send as 'Accepted' and then 'Delivered'.

### 4. Trigger Email Sends from App Actions

Connect the Mailgun API Call to actions in your FlutterFlow app. Identify the user interaction or app event that should trigger an email — for example, a 'Complete Registration' button tap, a successful Stripe payment confirmation, or a Supabase row insert completion. Click the widget or action that fires the event to open its Actions panel on the right side, then click '+ Add Action'. Choose 'Backend/API Call' from the action list. Select the 'MailgunEmail' group and the 'sendEmail' call. Map the variables: set `to` to the current user's email from Firebase Auth (`currentUserEmail`) or a widget state value, set `subject` to a string like 'Welcome to AppName!', and set `text` to a multi-line string with your email content. For personalized content like 'Hi John,' + message body + footer, use FlutterFlow's string concatenation or build the text in a Custom Function. Chain this action after your data-write action so the email only fires after the Supabase/Firestore write succeeds — use 'Action Chaining' in the Actions panel to control the order. You can also wrap the trigger in a Conditional Action to send different emails for different scenarios (e.g., email content differs for free vs paid plan signups).

**Expected result:** The app action triggers the API Call and an email is delivered to the user's inbox. The Mailgun Events log confirms the delivery with a 'delivered' status.

### 5. (Optional) Capture Delivery Events via Mailgun Webhooks

Mailgun can notify you when an email is delivered, bounced, complained about, or opened — useful for showing delivery status in your FlutterFlow app or flagging undeliverable addresses. Because FlutterFlow cannot receive incoming HTTP requests, you need a second Cloud Function that receives the Mailgun webhook and writes the event to Firestore. In the Mailgun dashboard, go to Sending → Webhooks and add a webhook for the events you care about (delivered, bounced, complained, opened, clicked). Set the webhook URL to a new Cloud Function endpoint (e.g., `handleMailgunEvent`). In that function, validate the Mailgun webhook signature using the webhook signing key (found in Settings → API Security), then write the event data to a Firestore collection like `email_events`. In your FlutterFlow app, bind a ListView or a status indicator to that Firestore collection filtered by the user's email address. This gives you a live delivery status view without polling Mailgun's API from the client. Free-tier Mailgun accounts retain event logs for 5 days; the Flex plan retains them for 30 days — plan your Firestore write strategy accordingly.

```
// Cloud Function — Mailgun webhook handler (index.js)
const functions = require('firebase-functions');
const admin = require('firebase-admin');
const crypto = require('crypto');

admin.initializeApp();
const db = admin.firestore();

function verifyMailgunSignature(signingKey, timestamp, token, signature) {
  const encodedToken = crypto
    .createHmac('sha256', signingKey)
    .update(timestamp.concat(token))
    .digest('hex');
  return encodedToken === signature;
}

exports.handleMailgunEvent = functions.https.onRequest(async (req, res) => {
  if (req.method !== 'POST') { res.status(405).send('POST only'); return; }
  const { timestamp, token, signature } = req.body['signature'] || {};
  const signingKey = functions.config().mailgun.webhook_signing_key;
  if (!verifyMailgunSignature(signingKey, timestamp, token, signature)) {
    return res.status(401).send('Invalid signature');
  }
  const eventData = req.body['event-data'] || {};
  await db.collection('email_events').add({
    event: eventData.event,
    recipient: eventData.recipient,
    timestamp: new Date(eventData.timestamp * 1000),
    messageId: eventData.message && eventData.message.headers['message-id'],
    createdAt: admin.firestore.FieldValue.serverTimestamp(),
  });
  res.status(200).send('OK');
});
```

**Expected result:** Mailgun webhook events (delivered, bounced) write to the Firestore `email_events` collection. A FlutterFlow ListView bound to that collection shows real-time delivery status for sent emails.

## Best practices

- Always proxy Mailgun through a Firebase Cloud Function — the Private API Key in a FlutterFlow API Call header ships inside the compiled app and can be extracted from any device.
- Use the correct regional endpoint from the start: US accounts use `api.mailgun.net`, EU accounts use `api.eu.mailgun.net`. Wrong region = silent send failures.
- Store the Mailgun Private API Key in Firebase Functions environment config or Secret Manager — never in FlutterFlow App Values or hardcoded in Dart.
- Send form-urlencoded to Mailgun (not raw JSON) — the Mailgun `/messages` endpoint only accepts `application/x-www-form-urlencoded` or `multipart/form-data`.
- Verify your sending domain in Mailgun and set up SPF/DKIM records to improve deliverability and avoid spam folder placement.
- For trial accounts, add all test email addresses as Authorized Recipients in Mailgun before testing — trial sends to unverified addresses are silently dropped.
- Chain the Mailgun send action after your data-write action in FlutterFlow so emails only fire when the backend write succeeds — avoids sending confirmation emails for failed transactions.
- Monitor the Mailgun Events log after sending to confirm delivery status — it shows whether messages were accepted, delivered, bounced, or complained about.

## Use cases

### SaaS app that emails a welcome message on new user registration

When a user completes registration in your FlutterFlow app, an API Call fires to a Cloud Function that sends a branded welcome email via Mailgun from your domain. The email includes the user's first name and a link to complete their profile, improving activation rates compared to generic system emails.

Prompt example:

```
Send a personalized welcome email via Mailgun when a user registers in the app, using their first name in the subject line and body.
```

### E-commerce app that sends order confirmation emails

After a purchase is confirmed in a FlutterFlow shopping app, a Mailgun email goes out with the order number, itemized list, total, estimated delivery date, and a support contact. All email content is assembled in the Cloud Function from order data passed by FlutterFlow.

Prompt example:

```
After a checkout is completed, send a Mailgun order confirmation email with the order details, item list, and delivery estimate.
```

### B2B app that sends weekly digest emails to team accounts

A FlutterFlow project management app triggers a Cloud Function on a schedule that reads team activity from Firestore and sends a digest email via Mailgun to each team's admin. The FlutterFlow app provides a 'Send digest now' button that calls the same function on demand.

Prompt example:

```
Send a weekly activity digest email via Mailgun to each team's admin, summarizing tasks completed, active members, and open items.
```

## Troubleshooting

### 401 Unauthorized when calling the Mailgun send endpoint

Cause: The Basic Auth username is wrong (must be the literal string `api`) or the wrong API key type is being used (validation key or webhook signing key instead of the Private API Key).

Solution: In your Cloud Function, confirm the Authorization header is built as: `'Basic ' + Buffer.from('api:' + MAILGUN_KEY).toString('base64')`. The username must be the four characters `api` — not your email address, not `apikey`, not your domain. The password is the Private API Key starting with `key-`.

```
Authorization: 'Basic ' + Buffer.from(`api:${MAILGUN_KEY}`).toString('base64')
```

### Email not delivered but Mailgun returns 200 OK with an 'Accepted' status in logs

Cause: The Mailgun account is using the EU region (`api.eu.mailgun.net`) but the Cloud Function is calling the US endpoint (`api.mailgun.net`), or vice versa. Authentication succeeds across regions but the send silently fails.

Solution: In the Mailgun dashboard, check your sending domain's region under Sending → Domains. Update the `MAILGUN_BASE` URL in your Cloud Function to match: `https://api.eu.mailgun.net/v3` for EU or `https://api.mailgun.net/v3` for US. Redeploy and retry.

### 400 Bad Request with message 'to parameter is not a valid address'

Cause: The email address passed in the `to` field is malformed, or Mailgun's trial account is rejecting sends to unverified addresses.

Solution: Validate the email address format in FlutterFlow before calling the API (use a regex validator on the text field). On a Mailgun trial account, you can only send to addresses added as 'Authorized Recipients' in Settings → Authorized Recipients. Upgrade to a paid plan or add the test address to the authorized list.

### XMLHttpRequest error when testing the Cloud Function API Call from FlutterFlow web preview

Cause: The Cloud Function is missing CORS headers, causing the browser to block the response in FlutterFlow's web Run Mode.

Solution: Add the CORS header in the Cloud Function response: `res.set('Access-Control-Allow-Origin', '*')` and handle OPTIONS preflight requests. The code example in Step 2 already includes this — if you wrote your own function, add it at the top of the handler.

```
res.set('Access-Control-Allow-Origin', '*');
if (req.method === 'OPTIONS') { res.status(204).send(''); return; }
```

## Frequently asked questions

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

Technically yes, but you should not for production apps. The Mailgun Private API Key would be embedded in the compiled Flutter app bundle and can be extracted from any device that has the app installed, giving anyone the ability to send email on your behalf and exhaust your sending quota. For internal tools or quick prototypes, a direct API Call is acceptable — but always use a Cloud Function proxy for apps distributed to real users.

### Why does Mailgun return 'parameter is not a valid address' for a valid-looking email?

This usually means either the email address has a typo, or your Mailgun trial account is restricting sends to unverified addresses. On the free/trial tier, you must add recipient addresses as 'Authorized Recipients' under Mailgun Settings → Authorized Recipients. Once you upgrade to a paid plan, you can send to any valid email address.

### How do I show email delivery status inside my FlutterFlow app?

FlutterFlow cannot receive Mailgun's event webhooks directly. The solution is a second Cloud Function that accepts Mailgun's webhook POST, validates the signature, and writes the event (delivered, bounced, etc.) to a Firestore collection. Your FlutterFlow app then binds a widget to that Firestore collection filtered by the relevant email address or message ID, giving users real-time delivery status.

### Does Mailgun support HTML email templates?

Yes — include an `html` parameter alongside or instead of `text` in the form-urlencoded send payload. Pass the HTML string from your Cloud Function, where you can use a JavaScript template literal or a templating library like Handlebars to inject dynamic values. Avoid passing long HTML strings from FlutterFlow variables — keep templates in the function for easier maintenance.

### What is the difference between this 'mailgun' page and the 'mailgun-api' page in the directory?

This page focuses on the core transactional send workflow — welcome emails, receipts, password resets — through a Cloud Function proxy. The mailgun-api page may cover advanced API features like batch sending, mailing lists, or domain management. If your goal is 'send email when X happens in my app,' you are in the right place.

---

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